Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
69 changes: 40 additions & 29 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,33 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
observability guard, priorGuard, derived treatment lines).
- **`lib/analysis.ts`** — `runAnalysis(doc)`: evaluates cohorts (per-study; cross-study
federation via study-qualified member ids), then hands each view to its kind's runner.
**`lib/views/<kind>.ts`** holds one runner per view kind (compare, survival, cox, …), registered
in `lib/views/index.ts`; each gets the run's context (`ViewRun` in `lib/views/run.ts`: the
document, evaluated cohorts, resolvers, the view's cohorts and studies) and returns its result;
`lib/views/shared.ts` holds what several runners use (`overLimit`, `answering`, `refuse`). A new
view kind is a new runner there, registered in `index.ts` (`RUNNERS` is keyed by `ViewKind`, the
`View` union's kinds, so a kind without a runner does not compile). A result is a `ViewResult`, a
union over kinds: the common fields (`ViewResultCommon`) plus its kind's own (`ViewResultFields` in
`lib/types.ts`, where a new kind declares its fields), so checking `kind` narrows it and reading
another kind's field does not compile. A drawn result (no `error`, not `empty`) carries its kind's
main field (`ViewResultMain`; a runner's success path must fill it), and `isDrawn(v)` is the check
after which that field is read without `!`; a figure takes `DrawnResultOf<"<kind>">`. A refusal or
error for a kind known only at run time is `plainResult(kind, {…})`. **Kinds never import each other:** a
kind's runner, figure and `<kind>-<part>.ts` modules import only one another and the shared
modules (`lib/views/`: run, shared, rows, covariates; `lib/figures/`: common, volcano, bars,
forest, km) — `local/kind-boundary` in `eslint.config.mjs` enforces it; what two kinds need moves
into a shared module, listed there. Kinds registered through `drawn()`
have their figure drawn from the result and a throw reported as the view's error.
- **View kinds** — **each kind is a folder, `lib/views/<kind>/`, plus one page file,
`app/lab/kinds/<kind>.tsx`, and a line in each of three registries.** The folder holds `types.ts`
(the kind's spec and result fields: it adds its entry to `ViewSpecs`, `ViewResultFields` and
`ViewResultMain` in `lib/types.ts` by `declare module`), `run.ts` (the runner), `sql.ts` (its
queries; `test/boundary.test.ts` sweeps every kind's), `method.ts` (its method record, invariant
12), `figure.ts` (its SVG) and `kind.ts` (its descriptor: figure, title, results text, help,
federation need, marks — `ViewKindDef` in `lib/views/kind-def.ts`). The page file is its
`KindUi` (`app/lab/kinds/kind-ui.ts`): its add-menu entry and why it is disabled, the view a new
one starts from, how many cohorts it draws, and its card — the shared card
(`app/lab/kind-cards.tsx`) with its subtitle, controls and body, or a `Card` of its own. The
registries are `lib/views/index.ts` (runners), `lib/views/kinds.ts` (descriptors) and
`app/lab/kinds/index.ts` (page files); each is typed by `ViewKind`, the `View` union's kinds, so
a kind missing from any of them does not compile, and everything else (`lib/view-svg.ts`,
`lib/result-text.ts`, `VIEW_HELP`, `viewNeed`, the add menu, the cards) reads them. A runner gets
the run's context (`ViewRun` in `lib/views/run.ts`: the document, evaluated cohorts, resolvers,
the view's cohorts and studies) and returns its result; `lib/views/shared.ts` holds what several
runners use (`overLimit`, `answering`, `refuse`, `askEachSite`). A result is a `ViewResult`, a
union over kinds: the common fields (`ViewResultCommon`) plus its kind's own, so checking `kind`
narrows it and reading another kind's field does not compile. A drawn result (no `error`, not
`empty`) carries its kind's main field (a runner's success path must fill it), and `isDrawn(v)` is
the check after which that field is read without `!`; a figure takes `DrawnResultOf<"<kind>">`. A
refusal or error for a kind known only at run time is `plainResult(kind, {…})`. Kinds registered
through `drawn()` have their figure drawn from the result and a throw reported as the view's
error. **Kinds never import each other:** a kind's files import only one another and the shared
modules (`lib/views/`: run, shared, rows, covariates, kind-def, text; `lib/figures/`;
`app/lab/kinds/`: kind-ui, parts) — `local/kind-boundary` in `eslint.config.mjs` enforces it, and
only the registries list every kind; what two kinds need moves into a shared module, listed there.
- **`lib/oncoprint.ts`** — spec → rows (ClickHouse fetch); **`lib/oncoprint-assemble.ts`** — rows →
render contract, pure (bitmask cells, NOT_PROFILED=-1, panel-aware denominators, staircase +
declarative `sort`, clinical tracks incl. numeric heat, `heatmap` molecular tracks incl. synthetic
Expand All @@ -63,17 +73,18 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
`scripts/oncoprint-cli.ts` and `python -m cohortlab` wrap all of this for local tables, and
`/oncoprint-lab/import` does the same in the browser (`POST /api/oncoprint/local`).
`lib/oncoprint-cluster.ts` orders columns by a Ward dendrogram for the `cluster:<profile>`
sort key; `lib/oncoprint-detail.ts` answers one column at a time for tooltips. **`lib/view-svg.ts`** — the same contract for every other view kind: the dispatcher over
`lib/figures/<kind>.ts`; used by the page (drawn in the browser) and by MCP `run_analysis`
sort key; `lib/oncoprint-detail.ts` answers one column at a time for tooltips. **`lib/view-svg.ts`** — the same contract for every other view kind: it draws
each kind's figure through its descriptor (`lib/views/kinds.ts`); used by the page (drawn in the browser) and by MCP `run_analysis`
for its figures (the oncoprint's image included).
- **View aggregates** — most view kinds after the first few (`genome`, `pathways`, `enrichment`, …)
query through the resolvers' `viewAggregate`/`viewAggregateMany`/`viewMeta`, whose SQL lives in
**`lib/federation-views.ts`** (re-exported by `lib/federation.ts`, so the boundary suite sweeps
it). Every local participant has those methods, a remote peer does not, so such a view is not
drawn over a peer's studies (`lib/view-coverage.ts`, below invariant 10).
Figures: **`lib/figures/<kind>.ts`**, one module per kind (the later kinds registered with their
title in `lib/figures/index.ts`), dispatched by **`lib/view-svg.ts`**; all browser-safe, laid out
with **`lib/chart/`** (see "Figures" under UI conventions); cards: `app/lab/kind-cards.tsx`.
Figures: **`lib/views/<kind>/figure.ts`**, drawn through the kind's descriptor (`kind.ts`, listed in
`lib/views/kinds.ts`) by **`lib/view-svg.ts`**; what several figures share is in `lib/figures/`; all
browser-safe, laid out with **`lib/chart/`** (see "Figures" under UI conventions); cards:
`app/lab/kinds/<kind>.tsx`.
- **Table views** (`table`, `mutations`, `timeline`'s patient list, `alterations`' list and three tables, and any later view that lists rows) — **every one must
support the same five things: pagination, search over its text, a filter per column (text
conditions or a value list for text, a range for numbers), sorting, and a CSV download of every
Expand All @@ -88,7 +99,7 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
`POST /api/view-page` (a page, or a column's values) and `POST /api/table` (the CSV) serve every
table view. A view holding several tables (`alterations`) names one with `table` in those
requests (`RunOpts.table`), and `pagedTable()` in `lib/views/rows.ts` picks it from the result.
- **The `alterations` view** (cBioPortal's patient-view genomic tables, runner `lib/views/alterations.ts`) — one
- **The `alterations` view** (cBioPortal's patient-view genomic tables, runner `lib/views/alterations/run.ts`) — one
patient's Mutations, Copy Number Alterations and Structural Variants as three table views in one card, each
with its own query under its own spec key (`mutations`, `cna`, `sv`); the top-level query is the patient list's.
Row sources in **`lib/patient-alterations.ts`** (one row per variant / call / event however many of the
Expand All @@ -97,7 +108,7 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
Its navigator is the timeline's: **`app/lab/patient-nav.tsx`** (‹ prev / next ›, the List/Search dialog,
← / →) serves every view that shows one patient of a cohort at a time, stepping by the result's `position`
(`listPosition()` in `lib/views/rows.ts`).
- **The `timeline` view** (cBioPortal's patient-view Timeline, runner `lib/views/timeline.ts`) — one
- **The `timeline` view** (cBioPortal's patient-view Timeline, runner `lib/views/timeline/run.ts`) — one
patient's clinical events as tracks, with its cohort's patients (a table view) to pick from. **`lib/timeline-tracks.ts`** is
the track model, ported from cbioportal-frontend's `cbioportal-clinical-timeline` package and
`patientView/timeline/timeline_helpers.tsx`: the portal's track order and `trackStructures`
Expand Down Expand Up @@ -156,8 +167,8 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
what it does not know. A change to the document's shape, or to what an unchanged document means,
bumps DOC_VERSION and adds an upgrade step there; never re-interpret an old shape anywhere else.
- **`lib/explore.ts`** — faceted study exploration, single + federated (`buildExploreFederated`).
- **The `composition` view** (runner `lib/views/composition.ts`, SQL `lib/composition.ts`, figure
`lib/figures/composition.ts`) — parts of a whole per case (cell types, cell states, signatures):
- **The `composition` view** (runner `lib/views/composition/run.ts`, SQL `lib/composition.ts`, figure
`lib/views/composition/figure.ts`) — parts of a whole per case (cell types, cell states, signatures):
a generic-assay profile's entities or 2–16 numeric clinical attributes (`custom:` included). Each
case's vector is built in the database (`casesCte`); every participant returns only sums
(`compositionMomentsSql`), and the per-case stacked bars come from `PatientLevelResolver.viewRows`
Expand Down Expand Up @@ -270,8 +281,8 @@ ClickHouse SQL; clients never send SQL. Live at https://cbiolab.vercel.app (UI a
code that needs a new migration. The in-process database migrates itself on first use.
- **`app/lab/page.tsx`** — the app: Explore (study checkboxes + facets) / Saved /
Analysis / Examples. Tabs move out into their own components as they are untangled
(`saved-tab.tsx`, `examples-tab.tsx`), and each view kind's card is its own component in
`view-cards.tsx`, reading the page through one `ViewCtx`; Explore's sections (study picker, bar,
(`saved-tab.tsx`, `examples-tab.tsx`), and each view's card (`view-cards.tsx`: its kind's
own or the shared one, from `app/lab/kinds/`) reads the page through one `ViewCtx`; Explore's sections (study picker, bar,
add-chart dialog, facet cards) are `explore-sections.tsx`, reading Explore's state and actions
from `use-explore-state.ts` (the draft, the selection, add-cohort mode) through `ExploreCtx`; the
Analysis tab's cohort column, view column, document and genes dialogs are `analysis-sections.tsx`,
Expand Down Expand Up @@ -607,7 +618,7 @@ ask. These hold for every view kind, and for the chat/MCP figures that share its
`.dt-wait` in `app/_table/DataTable.tsx`) and the count is dimmed with a spinner beside it; the
bar, chips and headers stay visible and usable, and an action that would act on the stale
answer (the download) waits, saying why. Such a view draws its own waiting state, so the page
must not cover its card (`drawsOwnWaiting` in `app/lab/kind-cards.tsx`, honoured by the lab and
must not cover its card (`drawsOwnWaiting` in its `app/lab/kinds/<kind>.tsx`, honoured by the lab and
`PortalTabs`). Its controls show and edit the query **the document holds**, not the last result's,
so a second edit made while the first is running keeps the first. A page not yet on hand is
loading from the render that asks for it — never a flash of "no rows" first.
Expand Down
3 changes: 2 additions & 1 deletion app/_lollipop/HotspotsBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
// The figure itself (lib/view-svg.ts) is the plot alone; a figure that travels without the app
// carries the summary and the selection in its caption instead.
import React, { useMemo } from "react";
import type { HotspotGene, ViewResultOf } from "../../lib/types.ts";
import type { ViewResultOf } from "../../lib/types.ts";
import { IMPACT_COLORS, IMPACT_TYPES, filterHotspotGene, cellOf, type AlterationCell, type ImpactType } from "../../lib/lollipop.ts";
import { lollipopPanelsSvg } from "../../lib/lollipop-svg.ts";
import { DrawAtWidth } from "../lab/figure.tsx";
import { fmtShare } from "../../lib/chart/format.ts";
import { X } from "lucide-react";
import { Button } from "../_ui/button";
import { TB } from "../_ui/toolbar.ts";
import type { HotspotGene } from "../../lib/views/hotspots/types.ts";

type Col = "driver" | "vus";
const COLS: Col[] = ["driver", "vus"];
Expand Down
3 changes: 2 additions & 1 deletion app/_oncoprint/OncoprintFigure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { setPngDpi } from "../../lib/png-dpi.ts";
import { api } from "../lab/api.ts";
import type { UploadBinding } from "../lab/custom-data-client.ts";
import type { OncoprintBody } from "../../lib/api-schemas.ts";
import type { OncoprintSpec } from "../../lib/types.ts";

import { Check, Download, Ellipsis, ImageDown, ListOrdered, LoaderCircle, Minus, Plus, Sheet, SlidersHorizontal, Tag, X } from "lucide-react";
import { Button } from "../_ui/button";
import { ErrorAlert } from "../_ui/error-alert.tsx";
Expand All @@ -30,6 +30,7 @@ import { OptionSelect } from "../_ui/choice.tsx";
import { Slider } from "../_ui/slider";
import { FIELD, TB } from "../_ui/toolbar.ts";
import { cn } from "../../lib/cn";
import type { OncoprintSpec } from "../../lib/views/oncoprint/types.ts";

/** one of the zoom group's buttons: the group draws the border */
const ZOOM = "h-full rounded-none text-muted-foreground hover:bg-muted hover:text-foreground";
Expand Down
3 changes: 2 additions & 1 deletion app/api/oncoprint/detail/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { OncoprintSpec } from "../../../../lib/types.ts";

import { parseBody, OncoprintDetailRequest } from "../../../../lib/api-schemas.ts";
import { columnDetail } from "../../../../lib/oncoprint-detail";
import type { OncoprintSpec } from "../../../../lib/views/oncoprint/types.ts";

export const maxDuration = 30;

Expand Down
3 changes: 2 additions & 1 deletion app/api/oncoprint/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { OncoprintSpec } from "../../../lib/types.ts";

import { parseBody, OncoprintRequest } from "../../../lib/api-schemas.ts";
import { cachedContract } from "../../../lib/next-cache.ts";
import { withRequestCustomData } from "../../../lib/custom-data-server.ts";
import { packContract } from "../../../lib/oncoprint-binary";
import { gzipSync } from "node:zlib";
import { contractToSvg } from "../../../lib/oncoprint-svg";
import { contractToTsv } from "../../../lib/oncoprint-tabular";
import type { OncoprintSpec } from "../../../lib/views/oncoprint/types.ts";

export const maxDuration = 120;

Expand Down
4 changes: 2 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ main.analysis-main { max-width: min(132rem, 100%); }
.row.g5 { gap: 0.5rem; }
.facettitle { font-size: var(--t-md); font-weight: 600; }

/* ---------------- view-card tables (app/lab/view-cards.tsx) ---------------- */
/* ---------------- view-card tables (app/lab/kinds/<kind>.tsx) ---------------- */
.vscroll { overflow-x: auto; }
.vtable { border-collapse: collapse; font-size: var(--t-md); width: 100%; }
.vtable th, .vtable td { padding: 0.3rem 0.6rem; }
Expand Down Expand Up @@ -1559,7 +1559,7 @@ td.camp-links { flex-direction: column; align-items: flex-start; gap: var(--s1);
.xc-cell { fill: var(--accent); }
.xc-brush { fill: color-mix(in srgb, var(--accent) 15%, transparent); stroke: var(--accent); stroke-dasharray: 3 2; }

/* the portal-style card bodies (app/lab/portal-bodies.tsx) */
/* the portal-style card bodies (app/lab/kinds/<kind>.tsx, on app/lab/portal-bodies.tsx) */
.pb-scroll { overflow-x: auto; }
.pb-row { display: flex; gap: var(--s4); align-items: flex-start; flex-wrap: wrap; }
.pb-row > * { min-width: 0; }
Expand Down
3 changes: 2 additions & 1 deletion app/lab/alterations-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import React, { useMemo, useState } from "react";
import { DataTable, specQuery } from "../_table/DataTable.tsx";
import { sampleMarks } from "../../lib/timeline-tracks.ts";
import type { AlterationsResult, TableQuery } from "../../lib/types";
import type { TableQuery } from "../../lib/types";
import { AnnotationBadge } from "./portal-bodies";
import { PatientNav, PatientsDialog, patientKeys, type PatientKey } from "./patient-nav.tsx";
import type { AlterationsResult } from "../../lib/views/alterations/types";

type Spec = { patient?: PatientKey } & Record<string, unknown>;

Expand Down
Loading
Loading