Skip to content

Reorganize view kinds: one folder per kind, three registries - #66

Open
alisman wants to merge 7 commits into
masterfrom
session/code-qual
Open

alisman wants to merge 7 commits into
masterfrom
session/code-qual

Conversation

@alisman

@alisman alisman commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Each view kind now lives in one place: a folder, lib/views/<kind>/, plus one page file, app/lab/kinds/<kind>.tsx. Each kind also has one line in each of three registries. Adding a view means adding that folder, that page file and those registry lines. Nothing else in the codebase lists kinds any more.

What moved

  • lib/views/<kind>/ holds the kind's code, split into these files:
    • types.ts adds the kind's entries to ViewSpecs, ViewResultFields and ViewResultMain with declare module.
    • run.ts is the runner.
    • sql.ts holds its queries. The boundary suite now imports every kind's sql.ts.
    • method.ts builds its method record.
    • figure.ts draws its SVG.
    • kind.ts is its descriptor (ViewKindDef): figure, title, results text, help, federation need, preview and new marks.
  • app/lab/kinds/<kind>.tsx is the kind's KindUi:
    • its add-menu entry, and why the button is disabled when it is;
    • the view a new one starts from;
    • how many cohorts it draws;
    • its card. That is either the shared card (kind-cards.tsx) with this kind's subtitle, controls and body, or a Card of its own. The twelve oldest kinds have their own.
  • Registries:
    • lib/views/index.ts lists the runners.
    • lib/views/kinds.ts lists the descriptors.
    • app/lab/kinds/index.ts lists the page files.
    • Each registry is typed by ViewKind, so a kind missing from any of them does not compile.
  • Lists replaced by the registries:
    • figure dispatch;
    • result-text dispatch;
    • VIEW_HELP, viewNeed, PREVIEW_KINDS, isNewKind;
    • the add menu;
    • addView defaults;
    • maxCohortsOf, drawsOwnHead, drawsOwnWaiting;
    • card titles, KindBody's switch, and the shared card's subtitle, controls and body chains.
    • KIND_CARDS, VIEW_HEAD_TITLES, lib/figures/index.ts and the unused GENE_VIEWS are gone.
  • local/kind-boundary (ESLint) stops one kind's files from importing another kind's. Code that several kinds use goes in a shared module, and every shared module is listed in the rule.
  • Combining answers: compare, cancertypes, overlap and composition now combine their sites' answers after all of them have arrived, in the view's order (askEachSite), like every other runner. Before, composition added up floating-point moments in whatever order studies answered, and overlap broke ties in that same order.

How we know nothing changed

Before anything moved, the first commit pinned every view card's HTML and every kind's page-facing facts:

  • the menu entry and its order;
  • defaults, limits, titles, subtitles, help and marks.

Those snapshots join the existing ones (SQL, result, SVG and results text for 53 cases, each run with answers delivered in order and in reverse).

Every snapshot is byte-identical through every commit. The one exception is the method records' source-file citations, which now name the new paths (9 lines, all intended).

  • npm test: 1614/1614 pass.
  • tsc, eslint and knip are clean.
  • scripts/gate.sh passes in full: build, unit, e2e, federation e2e, and the warehouse checks (619 portal comparisons, cohort logic, pooled, methods, denominators).

Notes

  • No behaviour change is intended anywhere. This PR moves code only.
  • timeline-body.tsx, alterations-body.tsx and the portal table stay where they were; the kind files import them.
  • CLAUDE.md's architecture map now describes "a new view = folder + page file + registry lines".

🤖 Generated with Claude Code

https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1

alisman and others added 7 commits October 23, 2026 22:23
…re reorganizing views

The view snapshots pinned what each view computes and draws, not the lab page's cards, and the
per-kind code about to move includes each card's controls, subtitle and body and the page's
per-kind rules. Two more checks, so the move has to leave those byte for byte too:

- <case>.html, a fifth file per snapshot case: each view's card rendered as the lab page draws it
  (app/lab/view-cards.tsx ViewCard: header, controls, body), from the same replayed result, so it
  costs no extra engine run and gets the reversed-delivery check. Drawn with a fixed stand-in page
  context (test/support/render-views.ts), without timings and the query list, and with each
  figure's SVG reduced to a mark: <case>.sql and <case>.svg already pin those. 0.8 MB in all.
- test/view-kinds.test.ts → test/snapshots/kinds.json: for every kind, its add-menu entry (label,
  why it can't be added, loading) for a study that has everything, one that has nothing and one
  answered aggregate-only; the spec a new view starts from (rich study, bare study, two cohorts);
  its card's title, limits, empty-view hint, add restrictions and subtitle; whether it draws its own
  header and waiting state; its help text, federation need, preview and NEW marks, and figure
  title. Read through the functions the page itself calls (useViewOptions, useViewEdits — plain
  functions, no React hooks inside — kind-cards.tsx, view-cards.tsx).

Node's type stripping cannot run JSX, so test/support/tsx-loader.mjs (a module loader registered by
those tests) compiles app/ files with esbuild (new dev dependency, 0.28.2) and resolves the app's
extension-less and "@/" imports as Next does; lib/ keeps node's own stripping.

Verified: npm test 1611/1611 (61 s); the HTML and kinds snapshots are identical across runs and
across sent/reversed delivery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
Every view kind's runner, figure, types, SQL and method record lived in shared files (types.ts,
federation-views.ts, method.ts, figures/) beside every other kind's, so each new view edited the
same hubs. Each kind now has a folder:
  run.ts      its runner (from lib/views/<kind>.ts; riskmodel's table1.ts and validate.ts too)
  figure.ts   its figure (from lib/figures/<kind>.ts)
  types.ts    its spec and result types, and its entries in lib/types.ts's maps
  sql.ts      the SQL only it uses (from lib/federation-views.ts)
  method.ts   its method record (from lib/method.ts)
A declaration moved only if nothing outside the kind's folder (tests aside) used it, together with
the helpers only it used; what two kinds share stays in the shared file (the lab-test, genome
coverage and panel-gene queries, the assay profile and entity queries, CoxTrace, …).

lib/types.ts no longer names a kind. `View` is ViewSpecs[keyof ViewSpecs], and ViewSpecs,
ViewResultFields and ViewResultMain start empty: each kind's types.ts adds its own entries with
`declare module "../../types.ts"`, so the type guarantees of #55/#57 hold unchanged.

The boundary suite now imports every lib/views/<kind>/sql.ts it finds and sweeps those builders
with lib/federation.ts's, so a kind's own SQL is held to "aggregates only" without being
registered. orgSiteMapSql is renamed orgSiteMapExpr: it builds an expression, not a query.
Tests that found the kinds by parsing `export type View =` read each kind folder's ViewSpecs
entry instead (test/support/view-kinds.ts).

Moved mechanically (TypeScript's language service for file renames, and a checker-driven mover
for declarations: each name a moved declaration uses imported exactly, every importer rewritten).
The only output change: method records cite where the code now is ("file":
"lib/views/attrition/run.ts" for "lib/views/attrition.ts"), 9 lines in 8 snapshots.

Verified: every view snapshot (SQL, result, figure, text, card HTML) and test/snapshots/kinds.json
otherwise byte-identical; npm test 1611/1611; tsc, eslint, knip clean; every lib module loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
…the folders

Six places listed something about every kind: lib/view-svg.ts's figure and title chains,
lib/figures/index.ts, lib/result-text.ts's fifty-branch text chain, VIEW_HELP, view-coverage's
NEEDS, PREVIEW_KINDS and NEW_KINDS. Each kind now says all of it in lib/views/<kind>/kind.ts (a
ViewKindDef, lib/views/kind-def.ts: figure, title, text, help, need, preview, isNew), and
lib/views/kinds.ts lists the descriptors, typed so a kind in the View union without one does not
compile. Those six places read the registry; lib/figures/index.ts is gone.

The pieces were cut out mechanically: each figure's "is there anything to draw" test, each title,
each text branch (the same body, given what it used from the loop as a TextContext; one that
had nothing to say for a result returns false and gets the same "no text rendering" line), each
help text, need, preview note and NEW mark. table and mutations shared one text branch that
switched on the kind; each now has its half. The older kinds' figure functions left
lib/figures/bars.ts and forest.ts (now only what those figures share) and lib/view-svg.ts (the
lollipop) for their own figure.ts; the table-row text helpers went to lib/views/text.ts.

local/kind-boundary now reads the folders: a kind's files (lib/views/<kind>/, and its card,
app/lab/kinds/<kind>.tsx) import one another, the shared view modules and anything outside
lib/views, never another kind's files or the registries; a shared view module (lib/views' top-level
modules, lib/figures/) imports no kind. One documented exception: lib/views/run.ts's types for the
riskmodel fitter the server hands in. mcp-views' source-text checks on the old chain now check the
descriptors (every kind has text, nearly every text surfaces its caveat, an unknown kind still says
it has no text).

Verified: every view snapshot (SQL, result, figure, text, card HTML) and kinds.json byte-identical;
npm test 1611/1611; tsc, eslint, knip clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
…e comments name the moved files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
… than how its source is laid out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
…d in one registry

A view kind is now a folder (lib/views/<kind>/) plus one page file and a line in each of three
registries. The page file is the kind's 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's subtitle, controls and body, or a card of its own for the twelve kinds drawn before the shared
card existed. app/lab/kinds/index.ts lists them, typed by ViewKind, so a kind without one does not
compile.

What used to be per-kind lists and switches across the page reads the registry instead: the add menu
(use-view-options.ts VIEW_KINDS), drawsOwnHead, the defaults in addView, maxCohortsOf and the empty
view's hint (use-view-edits.ts), the card titles, KindBody's switch, the shared card's subtitle,
controls and body chains, drawsOwnWaiting and figureView. KIND_CARDS, VIEW_HEAD_TITLES and the unused
GENE_VIEWS are gone. kind-cards.tsx is now the shared card and its dispatchers; the bodies that were
in portal-bodies.tsx move to their kinds, leaving the portal table and badges they share. What several
kinds' cards share is app/lab/kinds/parts.tsx; local/kind-boundary treats it and kind-ui.ts as shared
modules, and keeps kinds' page files from importing each other.

Verified: every view card's HTML (test/snapshots/*.html) and every kind's page facts
(test/snapshots/kinds.json — menu entry and order, defaults, limits, titles, subtitles, help) are
byte-identical; npm test 1611/1611; tsc, eslint, knip clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
…in the view's order

compare, cancertypes, overlap and composition summed each study's answer inside its callback, as it
landed. Their sums were integers, or sorted afterwards, so no snapshot moved, but composition's
moments are floating-point sums (their rounding depends on the order they are added) and overlap's
intersections broke ties by the order masks first arrived. All four now ask through askEachSite (or,
for overlap, whose sites are studies, one Promise.all) and combine in the view's order, as every other
runner does.

Verified: every view snapshot unchanged, including each case's reversed-delivery run; npm test passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuBjiBEjYispDSEYDZDWM1
@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cbiolab-genie Ready Ready Preview Oct 23, 2026 10:39pm UTC
cohort-lab Ready Ready Preview Oct 23, 2026 10:39pm UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview – cohort-lab — 0a3fd254 Deployed Oct 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant