diff --git a/app/_home/HomePage.tsx b/app/_home/HomePage.tsx
index 92318a79..68ce3755 100644
--- a/app/_home/HomePage.tsx
+++ b/app/_home/HomePage.tsx
@@ -3,7 +3,7 @@
// (query-flow.tsx). The selection is in the URL (?studies=a,b), so a refresh or a shared link
// lands on it.
import { useSearchParams } from "next/navigation";
-import { PortalHeader } from "../portal/portal-chrome.tsx";
+import { SiteHeader } from "../_ui/site-header.tsx";
import { QueryFlow } from "./query-flow.tsx";
/** the selection written back in place: ticking studies is not a history entry */
@@ -16,7 +16,7 @@ export function HomePage() {
const q = useSearchParams();
const studies = [...new Set((q.get("studies") || q.get("study") || "").split(",").map((s) => s.trim()).filter((s) => /^[A-Za-z0-9._-]+$/.test(s)))];
return <>
-
+
diff --git a/app/_ui/site-header.tsx b/app/_ui/site-header.tsx
new file mode 100644
index 00000000..92f87b33
--- /dev/null
+++ b/app/_ui/site-header.tsx
@@ -0,0 +1,39 @@
+"use client";
+// THE app's header, the same on every page: the name (home), which page this is, the same links in
+// the same order, a page's own tools (its assistant menu) and the sign-in.
+import LoginButton from "../lab/login-button.tsx";
+
+const LINKS = [
+ ["studies", "/", "studies"],
+ ["examples", "/lab?tab=examples", "examples"],
+ ["reproductions", "/reproductions", "paper reproductions"],
+ ["campaigns", "/campaign", "hypothesis campaigns"],
+ ["verification", "/verification", "how we verify"],
+ ["rationale", "/rationale", "rationale"],
+] as const;
+export type SiteLink = typeof LINKS[number][0];
+
+export function SiteHeader({ page, current, onLink, children }: {
+ /** which page this is, beside the name */
+ page: string;
+ /** the link this page is, marked as the current one */
+ current?: SiteLink;
+ /** a page that can open a link in place (the lab's own tabs) does, and says so by returning true */
+ onLink?: (link: SiteLink) => boolean;
+ /** the page's own tools, before the sign-in */
+ children?: React.ReactNode;
+}) {
+ return (
+
+
Your research question, in plain words, to your AI assistant — Claude in the Chrome sidebar.
diff --git a/app/datasets/layout.tsx b/app/datasets/layout.tsx
index d786d211..b81d3d45 100644
--- a/app/datasets/layout.tsx
+++ b/app/datasets/layout.tsx
@@ -1,14 +1,16 @@
// The data sets page reads the warehouse at request time, which under Cache Components must stream
// inside a Suspense boundary; the fallback holds the page's shape while the catalog loads.
import { Suspense } from "react";
+import { SiteHeader } from "../_ui/site-header.tsx";
export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
- return (
+ return (<>
+
>
+ ?
: setCounts((o) => o || c)} />}
diff --git a/app/portal/portal-chrome.tsx b/app/portal/portal-chrome.tsx
index 1074bb11..00296c5a 100644
--- a/app/portal/portal-chrome.tsx
+++ b/app/portal/portal-chrome.tsx
@@ -1,24 +1,10 @@
"use client";
-// The chrome the portal pages share: the app's header (the lab's own), the summary card over the
+// The chrome the portal pages share: the summary card over the
// tabs (the study, the query, the groups and their sizes).
import { SeriesSwatch } from "../lab/view-chrome.tsx";
import { Badge } from "../_ui/badge";
import { fmtInt, fmtPct } from "../../lib/chart/format.ts";
-/** the app's header, as the lab draws it, naming which page this is; the name goes home */
-export function PortalHeader({ page }: { page: string }) {
- return (
-
-
- {page}
-
- studies
- analysis
- how we verify
-
- );
-}
-
/** one group's swatch, name and size; the swatch is the colour the figures draw it in */
function GroupChip({ name, i, n }: { name: string; i: number; n?: number }) {
return (
@@ -86,3 +72,23 @@ export function PortalSummary({ eyebrow, title, study, studyTitle, detail, genes
);
}
+
+/**
+ * A portal page while it learns what its studies hold (which decides its tabs): shaped like what
+ * replaces it — the tab row, then a view card's title, toolbar and figure — so the page does not
+ * change shape on the way to its first view.
+ */
+export function PortalLoading() {
+ return (
+
- Published cancer-genomics findings, reproduced against the live database using this
- tool's own analysis-document DSL — no source changes, just the DSL as it stands today.
- Every number below was run live while this page's content was written; a rerun may
- drift slightly as the warehouse reloads, which each view's own caveat discloses rather
- than something this page freezes in place.
-
-
- Reassessed on 28 September 2026, after eight new view kinds landed. Three papers moved:
- Jonsson's 1p/19q co-deletion, blocked until now, reproduces from the copy-number segments,
- alongside its cell-cycle and recurrence findings; Razavi's endocrine-resistance genes
- (ESR1, NF1, ERBB2) reproduce against the closest proxy the study records; and Jee's
- tissue-versus-plasma finding is compared within patients. The rest were checked and are
- unchanged. The sarcoma study still has no copy-number data, and gastric cancer's
- immunotherapy outcomes are too sparse and oddly coded. Endometrial survival is simply not
- significant on this release. The prostate race analysis still needs a comparison held
- within a second attribute (stage), which none of the new views provides.
-
-
-
{REPRODUCTIONS.length}papers
-
{counts.full || 0}fully reproduced
-
{counts.partial || 0}partial
-
{counts.bugfound || 0}bug found
+
+
Paper reproductions
+
+ Published cancer-genomics findings, reproduced against the live database using this
+ tool's own analysis-document DSL — no source changes, just the DSL as it stands today.
+ Every number below was run live while this page's content was written; a rerun may
+ drift slightly as the warehouse reloads, which each view's own caveat discloses rather
+ than something this page freezes in place.
+
+
+ Reassessed on 28 September 2026, after eight new view kinds landed. Three papers moved:
+ Jonsson's 1p/19q co-deletion, blocked until now, reproduces from the copy-number segments,
+ alongside its cell-cycle and recurrence findings; Razavi's endocrine-resistance genes
+ (ESR1, NF1, ERBB2) reproduce against the closest proxy the study records; and Jee's
+ tissue-versus-plasma finding is compared within patients. The rest were checked and are
+ unchanged. The sarcoma study still has no copy-number data, and gastric cancer's
+ immunotherapy outcomes are too sparse and oddly coded. Endometrial survival is simply not
+ significant on this release. The prostate race analysis still needs a comparison held
+ within a second attribute (stage), which none of the new views provides.
+
-
-
+
+
+ >
);
}
diff --git a/app/verification/page.tsx b/app/verification/page.tsx
index bc4d69e8..fad71a75 100644
--- a/app/verification/page.tsx
+++ b/app/verification/page.tsx
@@ -5,6 +5,7 @@
// fixtures the statistics tests read, and from lib/verification.ts — so the page reports the last
// run, and says when that was, instead of restating claims by hand.
import { PageTools } from "../_agent/PageTools.tsx";
+import { SiteHeader } from "../_ui/site-header.tsx";
import { STAT_CHECKS, COXNET_CHECK, JUDGMENT_CALLS } from "../../lib/verification.ts";
import { VERIFIED } from "../../lib/method.ts";
import { REPRODUCTIONS } from "../../lib/reproductions.ts";
@@ -75,327 +76,323 @@ export default function VerificationPage() {
}
return (
-
-
- This page is the report of an automated verification suite: a set of programs that check
- this tool's numbers against things we did not write, namely cBioPortal and the standard
- statistics libraries. It is built to run in continuous integration (CI): automatically, on every
- proposed change to the code, blocking any change that fails a check and publishing each run's
- results as a report. The results below are a copy of that report saved with the code, from the
- run on {when(lastRun)}. For the most recent run, see{" "}
- the latest CI runs on master (on GitHub; it opens only for people with access
- to the code).
-
-
- Why this matters: much of this tool's code was written with an AI assistant. That is a reason to
- check its numbers, not to trust them, so nothing here asks you to trust the code. Every check
- compares against an independent source, every check can be rerun by anyone with the same access,
- and the checks have themselves been tested by planting bugs in the code and confirming each bug is
- caught.
-
-
-
{parity.agree}/{parity.total}counts match cBioPortal
-
{logic.pass}/{logic.total}cohort rules hold
-
{STAT_CHECKS.length + 1}groups of statistics match the standard libraries
{repro.full}/{repro.total}papers fully reproduced (checked by hand, not by the suite)
+
+
How we check the numbers
+
+ This page is the report of an automated verification suite: a set of programs that check
+ this tool's numbers against things we did not write, namely cBioPortal and the standard
+ statistics libraries. It is built to run in continuous integration (CI): automatically, on every
+ proposed change to the code, blocking any change that fails a check and publishing each run's
+ results as a report. The results below are a copy of that report saved with the code, from the
+ run on {when(lastRun)}. For the most recent run, see{" "}
+ the latest CI runs on master (on GitHub; it opens only for people with access
+ to the code).
+
+
+ Why this matters: much of this tool's code was written with an AI assistant. That is a reason to
+ check its numbers, not to trust them, so nothing here asks you to trust the code. Every check
+ compares against an independent source, every check can be rerun by anyone with the same access,
+ and the checks have themselves been tested by planting bugs in the code and confirming each bug is
+ caught.
+
+
+
{parity.agree}/{parity.total}counts match cBioPortal
+
{logic.pass}/{logic.total}cohort rules hold
+
{STAT_CHECKS.length + 1}groups of statistics match the standard libraries
{repro.full}/{repro.total}papers fully reproduced (checked by hand, not by the suite)
+
-
-
+
-
-
How the suite runs, and what it depends on
-
- The suite has five stages. On every proposed change, CI runs all of them, together with a build of
- the app and its end-to-end browser tests, and keeps the report as an artifact of the run. Anyone
- can run the same five stages with one command, npm run verify, which
- takes about ten minutes and stops with an error if any check fails. The report is written to{" "}
- docs/verification/; this page shows the copy saved with the code,
- updated by hand from a passing run. Every CI run on master keeps its own report, as a download
- named verification-report on its page on GitHub.
-
-
-
Stage
What it checks
What it needs
-
-
Statistics
Each statistical method against lifelines, statsmodels and scipy (section 3)
-
Nothing external: no network, no database. The reference answers are saved with the code.
The public cBioPortal API at www.cbioportal.org (no account or key), and the warehouse
-
Cohort logic
And / or / not, splits, lines of therapy, held-out halves (section 2)
-
The warehouse, including its OncoKB annotations for driver mutations
-
Method scripts
Each example analysis's Python script reproduces its numbers (section 4)
-
The warehouse, and Python with pinned versions of the statistics libraries
-
Planted bugs
Each planted bug makes at least one of the checks above fail (section 6)
-
Whatever the stages it reruns need
-
-
-
- The warehouse is this tool's own database: a copy of the public studies in cBioPortal,
- loaded into ClickHouse, which is the database every analysis in the app runs against. The suite
- only reads from it. In CI, the read-only login is supplied as a secret.
-
-
The studies the suite uses, chosen because each exercises something that is easy to get wrong:
- One known source of false alarms. The cBioPortal comparison needs both sides to hold the same
- release of each study. If cBioPortal updates a study before the warehouse is reloaded, the
- comparison fails even though nothing in the code is wrong. That failure is intended: the suite
- reports the difference rather than tolerating it, and someone has to check whether the cause is the
- data or the code.
-
-
+
+
How the suite runs, and what it depends on
+
+ The suite has five stages. On every proposed change, CI runs all of them, together with a build of
+ the app and its end-to-end browser tests, and keeps the report as an artifact of the run. Anyone
+ can run the same five stages with one command, npm run verify, which
+ takes about ten minutes and stops with an error if any check fails. The report is written to{" "}
+ docs/verification/; this page shows the copy saved with the code,
+ updated by hand from a passing run. Every CI run on master keeps its own report, as a download
+ named verification-report on its page on GitHub.
+
+
+
Stage
What it checks
What it needs
+
+
Statistics
Each statistical method against lifelines, statsmodels and scipy (section 3)
+
Nothing external: no network, no database. The reference answers are saved with the code.
The public cBioPortal API at www.cbioportal.org (no account or key), and the warehouse
+
Cohort logic
And / or / not, splits, lines of therapy, held-out halves (section 2)
+
The warehouse, including its OncoKB annotations for driver mutations
+
Method scripts
Each example analysis's Python script reproduces its numbers (section 4)
+
The warehouse, and Python with pinned versions of the statistics libraries
+
Planted bugs
Each planted bug makes at least one of the checks above fail (section 6)
+
Whatever the stages it reruns need
+
+
+
+ The warehouse is this tool's own database: a copy of the public studies in cBioPortal,
+ loaded into ClickHouse, which is the database every analysis in the app runs against. The suite
+ only reads from it. In CI, the read-only login is supplied as a secret.
+
+
The studies the suite uses, chosen because each exercises something that is easy to get wrong:
+ One known source of false alarms. The cBioPortal comparison needs both sides to hold the same
+ release of each study. If cBioPortal updates a study before the warehouse is reloaded, the
+ comparison fails even though nothing in the code is wrong. That failure is intended: the suite
+ reports the difference rather than tolerating it, and someone has to check whether the cause is the
+ data or the code.
+
+
-
-
1 Are the patient and mutation counts the same as cBioPortal's?
-
- cBioPortal has been used and checked by the cancer genomics community for over a decade, and it was
- built by a different team on separate code. We ask both tools the same questions about the same
- studies and require exactly the same answer. A count either matches or it does not, so no
- tolerance is allowed. The questions cover cohort sizes, how often each top gene is mutated,
- amplified, deleted or fused (both the number of altered samples and the number of samples
- sequenced for that gene), clinical counts, and filtered cohorts such as “patients with a TP53
- mutation”.
-
-
-
Study
Why it was chosen
Comparisons
Agree
-
- {byStudy.map((s) => (
-
-
{s.study}
-
{STUDY_NOTE[s.study] || ""}
-
{s.total}
-
{s.agree}
-
- ))}
-
-
-
- All {parity.total} comparisons
-
-
Study
What
This tool
cBioPortal
+
+
1 Are the patient and mutation counts the same as cBioPortal's?
+
+ cBioPortal has been used and checked by the cancer genomics community for over a decade, and it was
+ built by a different team on separate code. We ask both tools the same questions about the same
+ studies and require exactly the same answer. A count either matches or it does not, so no
+ tolerance is allowed. The questions cover cohort sizes, how often each top gene is mutated,
+ amplified, deleted or fused (both the number of altered samples and the number of samples
+ sequenced for that gene), clinical counts, and filtered cohorts such as “patients with a TP53
+ mutation”.
+
+
+
Study
Why it was chosen
Comparisons
Agree
- {parity.checks.map((c, i) => (
-
-
{c.study}
-
{PARITY_GROUPS[c.group] || c.group}: {c.what}
-
{c.ours}
-
{c.portal}
-
+ {byStudy.map((s) => (
+
+
{s.study}
+
{STUDY_NOTE[s.study] || ""}
+
{s.total}
+
{s.agree}
))}
-
-
Last run {when(parity.ranAt)} against {parity.reference}. Frequencies are shown as altered / sequenced for that gene.
-
+
+ All {parity.total} comparisons
+
+
Study
What
This tool
cBioPortal
+
+ {parity.checks.map((c, i) => (
+
+
{c.study}
+
{PARITY_GROUPS[c.group] || c.group}: {c.what}
+
{c.ours}
+
{c.portal}
+
+
+ ))}
+
+
+
+
Last run {when(parity.ranAt)} against {parity.reference}. Frequencies are shown as altered / sequenced for that gene.
+
-
-
2 Does a cohort contain the patients its definition says?
-
- No reference program can tell us who belongs in “never-smokers with a TP53 mutation who
- received immunotherapy”. What we can test is logic that must hold whatever the right answer
- is. For example, “A and B” must be exactly the patients found in both A and B, and a
- cohort plus its opposite must make up the whole study. The tool computes each side separately on
- real data, and the check compares the actual patient lists, not just the totals.
-
2 Does a cohort contain the patients its definition says?
+
+ No reference program can tell us who belongs in “never-smokers with a TP53 mutation who
+ received immunotherapy”. What we can test is logic that must hold whatever the right answer
+ is. For example, “A and B” must be exactly the patients found in both A and B, and a
+ cohort plus its opposite must make up the whole study. The tool computes each side separately on
+ real data, and the check compares the actual patient lists, not just the totals.
+
- Every statistical method the tool reports is checked against the library statisticians already
- use for it: lifelines, statsmodels and scipy, which are widely used in published analyses.
- Both are given the same datasets ({statCases.toLocaleString()} test cases in all). The test data
- is chosen to be awkward: tied death times, empty groups, very small and very large counts, patients
- who enter the study late. Our numbers must match the library's to the precision shown below,
- and the check runs before every release.
-
-
-
Method
Used in
Checked against
Must agree to
Cases
-
- {STAT_CHECKS.map((c) => (
-
-
{c.method}
-
{c.usedFor}
-
{c.keys.map((k) => VERIFIED[k].library + " " + VERIFIED[k].version).filter((x, i, a) => a.indexOf(x) === i).join(", ")}
-
{c.agreement}
-
{caseCount(c.fixture, c.caseKeys)}
+
+
3 Are the statistics computed correctly?
+
+ Every statistical method the tool reports is checked against the library statisticians already
+ use for it: lifelines, statsmodels and scipy, which are widely used in published analyses.
+ Both are given the same datasets ({statCases.toLocaleString()} test cases in all). The test data
+ is chosen to be awkward: tied death times, empty groups, very small and very large counts, patients
+ who enter the study late. Our numbers must match the library's to the precision shown below,
+ and the check runs before every release.
+
+
+
Method
Used in
Checked against
Must agree to
Cases
+
+ {STAT_CHECKS.map((c) => (
+
+
{c.method}
+
{c.usedFor}
+
{c.keys.map((k) => VERIFIED[k].library + " " + VERIFIED[k].version).filter((x, i, a) => a.indexOf(x) === i).join(", ")}
- Yes. Every result in the analysis app has a How this was computed panel. It lists each
- step from the database rows to the number on screen, the exact database queries that ran, and a
- short Python script that recomputes the result with the standard libraries. You can send that
- script to a statistician or run it yourself.
-
-
- The scripts are checked too. Before every release, the analyses in the app's Examples gallery
- ({EXAMPLES.length} of them) are run, and every script must reproduce the numbers the app showed.
- If the code changes how a result is computed but the script is not updated to match, the check
- fails.
-
-
+
+
4 Can I check a specific result myself?
+
+ Yes. Every result in the analysis app has a How this was computed panel. It lists each
+ step from the database rows to the number on screen, the exact database queries that ran, and a
+ short Python script that recomputes the result with the standard libraries. You can send that
+ script to a statistician or run it yourself.
+
+
+ The scripts are checked too. Before every release, the analyses in the app's Examples gallery
+ ({EXAMPLES.length} of them) are run, and every script must reproduce the numbers the app showed.
+ If the code changes how a result is computed but the script is not updated to match, the check
+ fails.
+
+
-
-
5 Does it find what published papers found?
-
- We took {repro.total} published cancer genomics papers whose data is in cBioPortal and re-derived
- their main findings from scratch with this tool. {repro.full} reproduce fully, {repro.partial}{" "}
- partly, and the rest are blocked by data the study does not include. Each one opens in the app
- as a runnable analysis, with the paper's claim written beside our result. One reproduction
- found a bug in this tool: a sarcoma study with no copy-number data reported MDM2 amplification
- as 0% instead of saying the data was missing. That bug has been fixed.
-
+ We took {repro.total} published cancer genomics papers whose data is in cBioPortal and re-derived
+ their main findings from scratch with this tool. {repro.full} reproduce fully, {repro.partial}{" "}
+ partly, and the rest are blocked by data the study does not include. Each one opens in the app
+ as a runnable analysis, with the paper's claim written beside our result. One reproduction
+ found a bug in this tool: a sarcoma study with no copy-number data reported MDM2 amplification
+ as 0% instead of saying the data was missing. That bug has been fixed.
+
- A check that always passes proves nothing. To show these checks would notice a real error, we
- planted {mutants.total} realistic bugs in the code, one at a time, and reran the checks after
- each. These are bugs that produce believable but wrong numbers rather than crashes. {mutants.caught}{" "}
- of {mutants.total} were caught.
- {shipped && <> One is a real bug this project once shipped: {shipped.replace(/^Shipped once: /, "")}{" "}
- The cBioPortal comparison now catches it.>}
-
+ A check that always passes proves nothing. To show these checks would notice a real error, we
+ planted {mutants.total} realistic bugs in the code, one at a time, and reran the checks after
+ each. These are bugs that produce believable but wrong numbers rather than crashes. {mutants.caught}{" "}
+ of {mutants.total} were caught.
+ {shipped && <> One is a real bug this project once shipped: {shipped.replace(/^Shipped once: /, "")}{" "}
+ The cBioPortal comparison now catches it.>}
+
- Matching cBioPortal shows we compute the same thing from the same data, not that the data
- is right, and not that cBioPortal is right. We found an example while building this page.
- In blca_mskcc_solit_2014, one copy-number dataset is labeled as
- whole-exome even though the samples were sequenced on a panel. Both tools therefore count each
- sample twice: copy-number frequencies are divided by 218 for a 109-sample study
- {blcaShared && <> (for example, {blcaShared.what}: {blcaShared.ours} in both tools)>}, so they
- appear half as common as they are. We have not changed this tool to differ from cBioPortal: the fix
- belongs in the study's data, where it corrects both.
-
-
- The checks cover the studies, genes and questions listed above, not every possible query.
- They were chosen to include the hard cases (several gene panels in one study, two copy-number
- datasets, timelines), but an untested combination could still be wrong.
-
-
- The statistics checks show the arithmetic matches the reference library. They do not show
- that a method is the right choice for your question. The choices we made are listed below so a
- statistician can review them.
-
-
- No software can check study design. Comparing patients who reached a third line of
- therapy with those who did not builds in survival bias (to reach a third line, a patient has to
- live long enough). The tool attaches caveats like this to its results where it can detect them,
- but interpreting a result is still the researcher's job.
-
-
- The paper reproductions are rerun by hand when they are revised, not automatically. As
- the database is updated, a rerun may give slightly different numbers.
-
-
-
+
+
What these checks do not show
+
+
+ Matching cBioPortal shows we compute the same thing from the same data, not that the data
+ is right, and not that cBioPortal is right. We found an example while building this page.
+ In blca_mskcc_solit_2014, one copy-number dataset is labeled as
+ whole-exome even though the samples were sequenced on a panel. Both tools therefore count each
+ sample twice: copy-number frequencies are divided by 218 for a 109-sample study
+ {blcaShared && <> (for example, {blcaShared.what}: {blcaShared.ours} in both tools)>}, so they
+ appear half as common as they are. We have not changed this tool to differ from cBioPortal: the fix
+ belongs in the study's data, where it corrects both.
+
+
+ The checks cover the studies, genes and questions listed above, not every possible query.
+ They were chosen to include the hard cases (several gene panels in one study, two copy-number
+ datasets, timelines), but an untested combination could still be wrong.
+
+
+ The statistics checks show the arithmetic matches the reference library. They do not show
+ that a method is the right choice for your question. The choices we made are listed below so a
+ statistician can review them.
+
+
+ No software can check study design. Comparing patients who reached a third line of
+ therapy with those who did not builds in survival bias (to reach a third line, a patient has to
+ live long enough). The tool attaches caveats like this to its results where it can detect them,
+ but interpreting a result is still the researcher's job.
+
+
+ The paper reproductions are rerun by hand when they are revised, not automatically. As
+ the database is updated, a rerun may give slightly different numbers.
+
+
+
-
-
Choices a statistician should review
-
Where there is more than one defensible method, this is the one the tool uses. Each is pinned by the checks in section 3.
-
- {JUDGMENT_CALLS.map((j) => (
-
{j.choice}
{j.why}
- ))}
-
-
+
+
Choices a statistician should review
+
Where there is more than one defensible method, this is the one the tool uses. Each is pinned by the checks in section 3.
+
+ {JUDGMENT_CALLS.map((j) => (
+
{j.choice}
{j.why}
+ ))}
+
+
-
-
Running the suite
-
- With network access, the warehouse login in .env.local, and{" "}
- uv for Python:
-
-
{[
- "npm run verify # every stage below; writes the report to docs/verification/",
- "",
- "npm test # statistics (no database or network needed)",
- "npm run check:portal # counts against cBioPortal",
- "npm run check:cohorts # cohort logic",
- "npm run check:methods # every example's Python script reproduces its numbers",
- "npm run check:mutants # plant each bug, confirm a check catches it (about three minutes)",
- ].join("\n")}
-
- The reference answers for the statistics are generated by the Python scripts in{" "}
- python/golden/, with library versions pinned, so a statistician can
- regenerate them independently.
-
-
-
+
+
Running the suite
+
+ With network access, the warehouse login in .env.local, and{" "}
+ uv for Python:
+
+
{[
+ "npm run verify # every stage below; writes the report to docs/verification/",
+ "",
+ "npm test # statistics (no database or network needed)",
+ "npm run check:portal # counts against cBioPortal",
+ "npm run check:cohorts # cohort logic",
+ "npm run check:methods # every example's Python script reproduces its numbers",
+ "npm run check:mutants # plant each bug, confirm a check catches it (about three minutes)",
+ ].join("\n")}
+
+ The reference answers for the statistics are generated by the Python scripts in{" "}
+ python/golden/, with library versions pinned, so a statistician can
+ regenerate them independently.
+
+
+
+ >
);
}
diff --git a/e2e.cjs b/e2e.cjs
index 73876d5a..28ed6f4d 100644
--- a/e2e.cjs
+++ b/e2e.cjs
@@ -167,7 +167,7 @@ async function main() {
page.url() + " status " + rootResp.status());
await page.goto(BASE + "/lab", { waitUntil: "domcontentloaded" });
await page.waitForSelector("[aria-label='lab sections'] [role=tab]", { timeout: 90000 });
- ok(scheme + ": app shell loads", (await page.getByRole("tab").count()) >= 4);
+ ok(scheme + ": app shell loads", (await page.getByRole("tab").count()) >= 3);
if (scheme === "light") {
// No credentials anywhere client-visible: check for env var names plus the
@@ -207,9 +207,9 @@ async function main() {
await page.waitForFunction(() => document.querySelector(".mermaid-diagram svg") !== null, null, { timeout: 30000 });
const svgCount = await page.locator(".mermaid-diagram svg").count();
const fallback = await page.locator(".mermaid-fallback").count();
- const backLink = await page.locator(".rationale-nav a").getAttribute("href");
- rationaleOk = svgCount > 0 && fallback === 0 && backLink === "/lab";
- rationaleInfo = "svg=" + svgCount + " fallback=" + fallback + " backLink=" + backLink;
+ const here = await page.locator("header a.toplink[aria-current=page]").innerText();
+ rationaleOk = svgCount > 0 && fallback === 0 && here === "rationale";
+ rationaleInfo = "svg=" + svgCount + " fallback=" + fallback + " header marks=" + here;
} catch (e) { rationaleInfo = String(e.message || e).slice(0, 100); }
ok("/rationale renders with a live Mermaid diagram", rationaleOk, rationaleInfo);
@@ -832,7 +832,7 @@ async function main() {
await page.goto(BASE + "/lab?tab=examples&doc=" + enc);
await page.waitForTimeout(1800);
ok("an explicit ?tab= beats the document's default tab",
- (await page.locator("[aria-label='lab sections'] [role=tab][aria-selected=true]").innerText()).trim() === "Examples");
+ (await page.locator("header a.toplink[aria-current=page]").innerText()).trim() === "examples");
// Cohorts tab: tick cohorts, submit them together. No per-row "add" links. The example
// cohorts live on the Examples page, so two saved cohorts are what get ticked here.
@@ -897,18 +897,21 @@ async function main() {
studyLines.length > 0 && studyLines.every(s => s && s.text && s.title),
studyLines.map(s => s ? s.text : "missing").join(" | "));
- // A kind already in the document shows as a selected pill, and clicking it removes the view.
- let pillOn = false, pillOff = false, left = [];
+ // The view buttons only add: a second click on a kind already in the document adds another of
+ // it (undone here, so the checks below see the document as it was).
+ let kinds2 = [], undone = [];
try {
- const pill = page.locator(".addview button:has-text('Lollipop')");
- pillOn = (await pill.getAttribute("aria-pressed")) === "true";
- await pill.click();
- await page.waitForFunction(() => !document.querySelector(".lp-plot circle.lollipop-head"), null, { timeout: 30000 });
- left = (JSON.parse(await page.locator("#docjson").inputValue()).views || []).map(v => v.kind);
- pillOff = (await pill.getAttribute("aria-pressed")) === "false";
+ const kinds = () => page.locator("#docjson").inputValue().then((t) => (JSON.parse(t).views || []).map(v => v.kind));
+ await page.locator(".addview button:has-text('Lollipop')").click();
+ await page.waitForFunction(() => (JSON.parse(document.querySelector("#docjson").value).views || []).filter(v => v.kind === "hotspots").length === 2, null, { timeout: 30000 });
+ kinds2 = await kinds();
+ await page.locator(".undo-doc").click();
+ await page.waitForFunction(() => (JSON.parse(document.querySelector("#docjson").value).views || []).filter(v => v.kind === "hotspots").length === 1, null, { timeout: 30000 });
+ undone = await kinds();
} catch { /* left as failed */ }
- ok("a selected view pill removes its view when clicked", pillOn && pillOff && !left.includes("hotspots") && left.length > 0,
- "selected " + pillOn + " · deselected " + pillOff + " · views now " + (left.join(", ") || "none"));
+ ok("a view button adds another view of a kind already in the document",
+ kinds2.filter(k => k === "hotspots").length === 2 && undone.filter(k => k === "hotspots").length === 1,
+ "after a second click: " + (kinds2.join(", ") || "none") + " · after undo: " + (undone.join(", ") || "none"));
// The oncoprint does not go through a resolver, so its provenance is a separate
// code path — and the one most likely to be left behind. It is also the one view
@@ -990,7 +993,7 @@ async function main() {
// The docs gate already proves each one RUNS; this proves the app can
// launch them and that the federated views actually paint something.
await page.goto(BASE + "/lab");
- await page.getByRole("tab", { name: "Examples", exact: true }).click();
+ await page.locator("header a.toplink", { hasText: /^examples$/ }).click();
await page.waitForSelector(".excard", { timeout: 120000 });
const nCards = await page.locator(".excard").count();
const { EXAMPLES } = await import("./lib/dsl-reference.ts");
@@ -1024,7 +1027,7 @@ async function main() {
];
// a multi-view document must render EVERY one of its views, not just the first
await page.goto(BASE + "/lab");
- await page.getByRole("tab", { name: "Examples", exact: true }).click();
+ await page.locator("header a.toplink", { hasText: /^examples$/ }).click();
await page.waitForSelector(".excard", { timeout: 120000 });
await page.locator(".excard").filter({ hasText: "One cohort, five ways" }).first()
.locator("button:has-text('Run')").click();
@@ -1102,7 +1105,7 @@ async function main() {
for (const [title, sel, label] of launches) {
await page.goto(BASE + "/lab");
- await page.getByRole("tab", { name: "Examples", exact: true }).click();
+ await page.locator("header a.toplink", { hasText: /^examples$/ }).click();
await page.waitForSelector(".excard", { timeout: 120000 });
const card = page.locator(".excard").filter({ hasText: title }).first();
await card.locator("button:has-text('Run')").click();