SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%

README: detailed project guide with brand mark, product map, architecture, data model, connectors, derived layer, metrics, API, CLI, setup, operations, gotchas, roadmap

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 13 days ago (Sep 11, 2026) parent d64e723

1 changed file +463 −68

modified README.md +463 −68
@@ -1,97 +1,492 @@
1 −# CancerIndex.io — the global index of cancer
1 +<p align="center">
2 + <img src="apps/web/public/brand/logo.svg" alt="CancerIndex — the global index of cancer" width="360">
3 +</p>
2 4
3 −CancerIndex is a provenance-first, continuously updated, transparently sourced oncology knowledge
4 −platform. It connects a canonical cancer ontology (NCIt-anchored, OncoTree and registry dimensions)
5 −to epidemiology, genomics, biomarkers, therapies, clinical trials, regulatory evidence and literature
6 −— for every recognised malignant disease entity — and publishes reproducible rankings with their
7 −scope, formula version and lineage down to the raw record.
5 +<h1 align="center">CancerIndex.io</h1>
8 6
9 −It is not a physician, not a diagnostic tool, and gives no treatment recommendations.
7 +<p align="center"><strong>Understand cancer through data.</strong><br>
8 +A provenance-first, continuously updated, transparently sourced index of every recognized cancer entity —
9 +epidemiology, genomics, biomarkers, drugs, regulatory approvals, clinical trials, research and derived
10 +intelligence, cross-linked in one coherent dataset.</p>
10 11
11 −## Status
12 +<p align="center">
13 + <a href="https://www.cancerindex.io">www.cancerindex.io</a> ·
14 + <a href="https://www.cancerindex.io/api/v1/docs">API docs</a> ·
15 + <a href="https://www.cancerindex.io/methodology">Methodology</a> ·
16 + <a href="https://www.cancerindex.io/sources">Sources</a> ·
17 + <a href="https://www.cancerindex.io/pulse">Pulse</a>
18 +</p>
12 19
13 −Phase 1 (2026-09). Foundation, connectors, public API, worker and web app are being built in
14 −parallel. Data currently ingested locally: OncoTree (865 cancer entities), with NCIt EVS, HGNC,
15 −CIViC, GDC, ClinVar, ClinicalTrials.gov, PubMed and CDC WONDER connectors in progress. GLOBOCAN
16 −remains under license review and SEER awaits credentials — the platform says so rather than showing
17 −numbers it cannot source.
20 +---
18 21
19 −## Repository
22 +## Table of contents
23 +
24 +1. [What CancerIndex is](#what-cancerindex-is)
25 +2. [What it refuses to do](#what-it-refuses-to-do)
26 +3. [The index in numbers](#the-index-in-numbers)
27 +4. [Product map](#product-map)
28 +5. [Architecture](#architecture)
29 +6. [Repository layout](#repository-layout)
30 +7. [Data model](#data-model)
31 +8. [Connectors](#connectors)
32 +9. [Derived intelligence layer](#derived-intelligence-layer)
33 +10. [Rankings and metrics](#rankings-and-metrics)
34 +11. [Public API](#public-api)
35 +12. [Operator CLI](#operator-cli)
36 +13. [Local development](#local-development)
37 +14. [Configuration](#configuration)
38 +15. [Quality: tests, QA, accessibility, performance](#quality-tests-qa-accessibility-performance)
39 +16. [Deployment and operations](#deployment-and-operations)
40 +17. [Brand](#brand)
41 +18. [Design language](#design-language)
42 +19. [Known gotchas](#known-gotchas)
43 +20. [Roadmap](#roadmap)
44 +21. [Licensing, attribution and contact](#licensing-attribution-and-contact)
45 +
46 +---
47 +
48 +## What CancerIndex is
49 +
50 +CancerIndex organizes fragmented oncology data into one structured, navigable system. A reader moves
51 +from a **cancer** to its **incidence and mortality**, to the **genes** altered in it, to a **variant**, to the
52 +**drugs** that target it, to the **regulatory approvals** of those drugs in each jurisdiction, to the
53 +**clinical trials** that investigate them and to the **papers** that describe them — every step a link, every
54 +number a sourced record.
55 +
56 +It is built for researchers, students, journalists, policymakers, biotech professionals and informed
57 +members of the public. It feels like a research terminal, not a health-content website: dense tables,
58 +sparse charts, one accent colour, a source badge on every figure.
59 +
60 +Five things make it different from a cancer information site:
61 +
62 +- **A canonical taxonomy first.** 9 510 active disease entities anchored on the NCI Thesaurus, with
63 + OncoTree, ICD-10, ICD-O, DOID, UMLS, MeSH and MONDO cross-references; 36 mutually exclusive
64 + top-level site groups (GLOBOCAN / ICD-10 ranges) for burden rankings without double counting.
65 +- **Provenance on every value.** Each observation, edge, approval and count points to a `provenance`
66 + row (source, dataset, version, URL, retrieval date, population, methodology); raw payloads are kept
67 + in a gzip JSON lake and can be replayed.
68 +- **Layers that stay separable.** RAW → NORMALIZED → CANONICAL → DERIVED → RANKED. Derived values
69 + carry a `formula_version` and the inputs that produced them ("Why this rank?").
70 +- **Claim categories that never merge.** Observed · published · curated · regulatory · guideline ·
71 + computed · AI-generated — labelled everywhere, mixed nowhere.
72 +- **No invented numbers.** Missing data is shown as *Data not yet available*. No estimates, no
73 + placeholder statistics, no composite "worst cancer" score.
74 +
75 +## What it refuses to do
76 +
77 +- It is **not a physician**: no diagnosis, no individual prognosis, no dosing, no treatment
78 + recommendation. Population statistics never predict an individual outcome and the UI says so.
79 +- It does not rank cancers by an opaque composite; each ranking is one metric, one scope, one formula.
80 +- It does not infer facts: no edge is created by a language model, no reason for a terminated trial is
81 + guessed, no indication is derived from an ATC class, no cancer is attached to an approval unless the
82 + official text names exactly one.
83 +- It does not redistribute data it may not redistribute: every connector manifest is licence-reviewed
84 + before it goes live (IARC / GLOBOCAN stays under review, SEER awaits credentials).
85 +
86 +## The index in numbers
87 +
88 +Production database on 2026-09-11 (`SELECT count(*)` — live counts, never estimates):
89 +
90 +| Layer | Entity | Count |
91 +|---|---|---:|
92 +| Taxonomy | active malignant cancer entities (of 9 510 active entities) | 5 595 |
93 +| Taxonomy | top-level site groups (ranking scope) | 36 |
94 +| Genomics | genes (HGNC) | 45 170 |
95 +| Genomics | variants (CIViC, ClinVar) | 417 000 |
96 +| Genomics | curated CIViC evidence items | 11 984 |
97 +| Biomarkers | canonical biomarkers (NCIt-verified) | 54 |
98 +| Drugs | canonical drugs (CIViC, ChEMBL, openFDA, Health Canada, EMA) | 814 |
99 +| Regulatory | approval records — US 1 570 · CA 1 534 · EU 459 | 3 563 |
100 +| Trials | ClinicalTrials.gov oncology studies | 126 238 |
101 +| Trials | registrant-entered study locations (1.19 M geocoded) | 1 212 144 |
102 +| Literature | PubMed records linked to entities (plus 19 080 per-cancer count windows) | 4 287 |
103 +| Epidemiology | US observations (CDC WONDER, U.S. Cancer Statistics), 1999–2024 | 10 674 |
104 +| Graph | active knowledge edges with cancer context | 18 822 |
105 +| Derived | trial-intelligence rows · drug-pipeline rows · research-gap components | 2 240 · 5 498 · 2 966 |
106 +| Derived | country × cancer × phase trial-site aggregates | 11 805 |
107 +| Rankings | current snapshots over 25 metric definitions | 875 |
108 +| Provenance | source records · provenance rows | 629 889 · 106 263 |
109 +| Sources | connectors registered (17 active, 1 under licence review, 1 awaiting credentials) | 19 |
110 +| Storage | PostgreSQL 17 | 2.5 GB |
111 +
112 +## Product map
113 +
114 +| Area | Route(s) | What it shows |
115 +|---|---|---|
116 +| Home | `/` | Hero, live ticker (cancers, active trials, recruiting Phase III, approved drugs), US burden, data-explorer chart, trial intelligence, new approvals, rankings preview, fastest-rising incidence, gap ratios, curated evidence; aside: where trials recruit, graph teaser, taxonomy, rare spotlight, sources |
117 +| Cancer profile | `/cancer/[slug]` + tabs `statistics` `survival` `genomics` `evidence` `drugs` `trials` `research` `rankings` `sources` | Header with codes and badges; observations by geography/year/sex with charts; cohort alteration frequencies with denominators; CIViC evidence with native levels; jurisdiction-aware approvals; trials with an intelligence strip; literature; "Why this rank?"; every source behind the page |
118 +| Cancers, taxonomy | `/cancers`, `/taxonomy` | Filterable entity table (level, type, malignant, hematologic, pediatric, rare); tree browser across hierarchy types |
119 +| Data explorer | `/explore`, `/explore/coverage`, `/api/export/epidemiology.csv` | Metric × cancers × geography × sex × age × years → comparable chart groups (metric, unit, geography, source, standard population, age never mixed), observations table, permalink, CSV with attribution, API call; coverage matrix |
120 +| Countries | `/countries`, `/country/[slug]` | Latest-year summary, top cancers per metric, trends, clinical trial activity (sites by cancer and phase), the jurisdiction's approval records, sources |
121 +| Compare | `/compare?ids=` | 2–4 cancers side by side (registry figures, counters, ranks) |
122 +| Trials | `/trials`, `/trial/[nct]`, `/trials/intelligence`, `/trials/terminated`, `/trials/map`, `/api/export/trial-intelligence.csv` | Search and filters; study record with mappings, locations, publications; per-cancer trial metrics (growth, enrollment, sponsor and country concentration, termination share, trials per 1 000 deaths); failure tracking with registrant-stated reasons classified by explicit rules; Equal Earth choropleth of sites |
123 +| Drugs | `/drugs`, `/drug/[slug]`, `/pipeline` | Canonical drugs with brands as aliases; identifiers (ATC, DIN, UNII, ChEMBL…), approvals by jurisdiction, development stage per cancer, evidence, trials; pipeline funnel per cancer |
124 +| Approvals | `/approvals` | Dated feed of FDA, Health Canada and EMA records grouped by month, filters by authority, jurisdiction, cancer, status, year |
125 +| Genes, variants | `/genes`, `/gene/[symbol]`, `/variant/[slug]` | HGNC genes; evidence by cancer; variants with ClinVar interpretations; cohort frequencies; publications |
126 +| Biomarkers | `/biomarkers`, `/biomarker/[slug]` | 54 curated biomarkers with NCIt codes verified against EVS; derived cancers, drugs, approvals (tumour-agnostic from real rows), trials, publications |
127 +| Rankings | `/rankings`, `/rankings/[metric]` | One metric, one scope, one formula version per table; lineage on every row; CSV export |
128 +| Research gap | `/research-gap`, `/api/export/research-gap.csv` | Death share vs trial and publication shares, log₂ gap ratios, per-1 000-deaths intensities, log-log scatter |
129 +| Knowledge graph | `/graph?focus=type:ref` | Contextual neighbourhood (source-native edges vs derived registry links), cancer → gene → variant → drug → approval → trials paths, accessible edge table |
130 +| Pulse, history | `/pulse`, `/year/[year]`, `/data-updates` | What changed (approvals, new recruiting Phase III, registration momentum, ranking moves, publications, dataset refreshes); a year in cancer 1999–now; connector state and ingestion log |
131 +| Sources, method | `/sources`, `/source/[slug]`, `/methodology`, `/methodology/trial-map`, `/trust`, `/about`, `/developers`, `/data` | Licence registry and connector health; every formula and threshold; policies; team and hosting; API guide; redistributable downloads |
132 +| Search | ⌘K / Ctrl+K anywhere, `/search` | Cancer, gene, variant, drug, trial, publication, source — exact › alias › prefix › fuzzy |
133 +| Admin | `/admin/*` (token) | Connectors, runs, unresolved labels, rankings, trace |
134 +
135 +Every page has a light and a dark theme (toggle in the header, `prefers-color-scheme` by default),
136 +a canonical URL, structured metadata and a server-rendered Open Graph / Twitter image.
137 +
138 +## Architecture
139 +
140 +```
141 + ┌──────────────────────── sources (19 connectors) ────────────────────────┐
142 + │ NCIt EVS · OncoTree · HGNC · CIViC · ClinVar · GDC · cBioPortal · MeSH │
143 + │ ClinicalTrials.gov · PubMed · CDC WONDER · CDC USCS · ChEMBL · openFDA │
144 + │ Health Canada DPD · EMA · (IARC GLOBOCAN: review) · (SEER: credentials) │
145 + └───────────────┬───────────────────────────────────────────────────────────┘
146 + │ HTTP / bulk files, rate-limited, restartable cursors
147 + ▼
148 + workers/main.ts ──► packages/connectors (SDK: manifest · HttpClient · RawLake · RunContext)
149 + pg-boss scheduler │ RAW → data/raw/{source}/{date}/{entity}/*.jsonl.gz + source_records
150 + cron per manifest │ NORMALIZED → validators, schema-drift stats, unresolved_labels
151 + counters 06:00 UTC │ CANONICAL → cancers · genes · variants · drugs · trials · publications
152 + intel 06:15 UTC │ observations · approvals · knowledge_edges · provenance
153 + rank 06:30 UTC ▼
154 + PostgreSQL 17 (+ pg_trgm, unaccent, pgvector) — Drizzle schema, snake_case
155 + │
156 + packages/ranking: counters → intelligence → rankings (DERIVED / RANKED)
157 + │
158 + ┌────────────────────┴────────────────────┐
159 + ▼ ▼
160 + apps/api Fastify 5 · /v1 · zod · OpenAPI apps/web Next.js 16 (webpack) · server components
161 + envelope { data, sources, dataRelease } Tailwind v4 tokens · pure SVG charts · dark mode
162 + rate limits · API keys · x-request-id /api/v1/* proxied to the API · ISR caching
163 + └────────────────────┬────────────────────┘
164 + ▼
165 + MacLustr node M4M64b · PM2 (web :8250, api :8251, worker, backup)
166 + MacLustr Tunnel (WireGuard + Caddy on BHS64) → https://www.cancerindex.io
167 +```
168 +
169 +**Principles baked into the code**
170 +
171 +- *Reconciliation before ingestion.* Labels map to entities by shared identifier, then curated alias,
172 + then normalized string; every mapping stores a `match_type` (`EXACT_IDENTIFIER`, `CURATED_EXACT`,
173 + `ONTOLOGY_EXACT`, `CURATED_BROADER`, `ALIAS`, `PROBABILISTIC`, `UNRESOLVED`). Unknown labels go to
174 + `unresolved_labels`, never to `/dev/null`.
175 +- *Time-aware observations.* A value for year X never overwrites year Y; multi-year aggregates keep
176 + both bounds; rates keep their standard population.
177 +- *Idempotent, restartable connectors.* Payload hashes, cursor checkpoints every 2 000 records or 60 s,
178 + SIGTERM-safe aborts, anomaly guard refusing destructive updates when a source shrinks by more than half.
179 +- *Deterministic derived layers.* Counters, intelligence tables and rankings are full rebuilds in one
180 + transaction; snapshots are immutable and keep `previous_rank` for change explanation.
181 +
182 +## Repository layout
183 +
184 +```
185 +apps/
186 + web/ Next.js 16 app (React 19, server components, Tailwind v4, --webpack)
187 + src/app/ routes (see Product map), opengraph-image.tsx per entity, icon.svg, manifest.ts
188 + src/components/ ui · charts (SVG) · cancer tabs · graph · explorer · country · home modules · layout
189 + src/lib/ db · format · queries/* (server-only SQL) · graph-model · explorer-* · og.tsx · site.ts
190 + src/assets/fonts/ WOFF copies of Newsreader / Inter / IBM Plex Mono for server-side images
191 + public/brand/ logo.svg · logo-mark.svg · logo-mark-dark.svg · PNG icons
192 + qa/smoke.mjs read-only smoke suite (44 routes, weight caps, mobile overflow, console errors)
193 + api/ Fastify 5 public API (/v1), zod schemas, OpenAPI at /v1/docs, admin routes
194 +packages/
195 + shared/ CI-XXX-00000001 ids, provenance types, normalization, logger, env
196 + database/ Drizzle schema (12 files, ~50 tables), migrations 0000–0002, seeds (metrics,
197 + geographies, biomarkers), alerts, ids
198 + ontology/ qualifier rules, CancerResolver (alias/code reconciliation), TOP_LEVEL_CANCERS
199 + connectors/ SDK (manifest · HttpClient · RawLake · RunContext · validators · doctor) and
200 + connectors/<id>/{manifest.ts,index.ts,normalize.ts,fixtures/,*.test.ts}
201 + ranking/ counters · intelligence (trial-intelligence, trial-sites, drug-pipeline,
202 + drug-duplicates, research-gap) · engine (snapshots) · trace · country-codes
203 +workers/ pg-boss scheduler and job handlers
204 +scripts/ci.ts operator CLI (`pnpm cix …`)
205 +deploy/ mld manifest, first-run bootstrap, backup / restore scripts
206 +docs/ ARCHITECTURE · DATA-MODEL · METHODOLOGY · API · SECURITY · AI · source-policy ·
207 + schema-changes-ops · adr/ (6 ADRs) · connectors/ (18 pages) · methodology/ (7 pages)
208 +CLAUDE.md operational rules every contributor (human or agent) follows
209 +```
210 +
211 +## Data model
212 +
213 +Source of truth: `packages/database/src/schema/*.ts` (documented in `docs/DATA-MODEL.md`). Public
214 +identifiers are `CI-<NS>-00000001` minted per namespace (`CAN`, `GENE`, `VAR`, `DRUG`, `TRIAL`, `PUB`,
215 +`BIO`, `STUDY`, `METRIC`, `SOURCE`, `GEO`, `PROV`…) — never database integers.
216 +
217 +| Group | Tables | Notes |
218 +|---|---|---|
219 +| Registry & operations | `sources`, `ingest_runs`, `connector_cursors`, `connector_field_stats`, `source_records`, `provenance`, `unresolved_labels`, `change_events`, `audit_log`, `entity_merges`, `system_alerts`, `api_keys`, `id_sequences` | licence status per source; one row per run with counters and log; raw-lake index with payload hashes; merge queue (proposed, never automatic) |
220 +| Cancer ontology | `cancers`, `cancer_aliases`, `cancer_hierarchy` (ncit · oncotree · anatomical · …), `cancer_codes`, `anatomical_sites`, `cancer_anatomy`, `geographies`, `cohort_definitions` | one row per disease concept, aliases and codes searchable, several trees coexist |
221 +| Genomics | `genes`, `gene_aliases`, `variants`, `variant_aliases`, `variant_clinical_significance`, `biomarkers`, `genomic_cohorts`, `cancer_gene_frequencies`, `entity_embeddings` | coordinates carry their assembly; frequencies carry their denominator |
222 +| Drugs & regulatory | `drugs`, `drug_aliases`, `drug_codes`, `treatment_regimens`, `drug_approvals` | brands are aliases; approvals are per jurisdiction, authority, application/DIN/EMA number, dated, with status and verbatim source status |
223 +| Trials | `clinical_trials`, `trial_conditions`, `trial_interventions`, `trial_locations`, `trial_pulse` | conditions and interventions reconciled with `match_type`; 1.2 M locations |
224 +| Literature | `publications`, `publication_entity_edges`, `literature_counts` | the exact PubMed query is stored with every count |
225 +| Evidence & graph | `knowledge_edges`, `civic_evidence_items`, `risk_factors` | edges carry cancer context, direction, native evidence level, category, provenance ids, status (`active` · `superseded`) |
226 +| Epidemiology | `epidemiology_observations`, `survival_observations` | metric, unit, sex, age group, year(s), CI, standard population, estimate type, site definition |
227 +| Derived | `entity_counters`, `trial_intelligence`, `trial_site_country_counts`, `drug_pipeline`, `research_gap_components`, `metric_definitions`, `ranking_snapshots`, `rankings`, `ai_answers` | every row has a `formula_version` and `inputs` |
228 +
229 +Schema changes are migrations generated with `pnpm db:generate` (one per integration) and applied by
230 +`pnpm db:migrate`, which also creates the extensions and the trigram / GIN performance indexes.
231 +
232 +## Connectors
233 +
234 +Each connector is a class with a manifest (licence, terms review date, documentation verification
235 +date, rate limits, schedule, anomaly guard), a health check and a `sync()`; it is tested against
236 +sanitized fixtures (normal, empty, pagination, rate-limit, server error, malformed) and documented in
237 +`docs/connectors/<id>.md`. `pnpm cix sources:sync` seeds manifests into `sources`; `/sources` shows them.
238 +
239 +| Connector | Organization | Category | Licence status | What it feeds |
240 +|---|---|---|---|---|
241 +| `ncit-evs` | NCI EVS | terminology | approved (CC BY 4.0) | cancers, aliases, codes, hierarchy — the ontology backbone |
242 +| `oncotree` | MSKCC | terminology | approved (CC BY 4.0) | OncoTree codes and tree mapped onto NCIt concepts |
243 +| `mesh` | NLM | terminology | approved | MeSH headings as aliases (PubMed queries, EMA therapeutic areas) |
244 +| `hgnc` | HGNC | genes | approved (CC0) | 45 k gene records and aliases |
245 +| `civic` | CIViC | evidence | approved (CC0) | evidence items, variants, therapies (mints drugs), edges |
246 +| `clinvar` | NCBI | variants | approved (public domain) | 417 k variants with interpretations |
247 +| `gdc` | NCI GDC | genomics | approved (open tier) | TCGA cohorts and gene alteration frequencies |
248 +| `cbioportal` | cBioPortal | genomics | approved | additional cohorts and frequencies |
249 +| `clinicaltrials` | ClinicalTrials.gov | trials | approved (public domain) | 126 k studies, conditions, interventions, locations, `trial_pulse`; intervention → drug reconciliation |
250 +| `pubmed` | NLM | literature | approved | per-cancer literature counts (query stored) and linked records |
251 +| `cdc-wonder` | CDC | epidemiology | approved | US mortality 1999–2024 |
252 +| `cdc-uscs` | CDC | epidemiology | approved | US incidence and mortality (USCS), sex-specific handling |
253 +| `chembl` | EMBL-EBI | drugs | approved (CC BY-SA) | drug enrichment (ids, mechanism, targets) |
254 +| `openfda` | US FDA | regulatory | approved (CC0) | Drugs@FDA applications and labels → US approval rows, indication-text cancer mapping, edges |
255 +| `health-canada-dpd` | Health Canada | regulatory | approved (OGL Canada) | Drug Product Database (ATC L01/L02/L03/V10) → DIN-level Canadian records, drug minting, codes |
256 +| `ema` | European Medicines Agency | regulatory | approved (attribution) | medicines data xlsx → EU authorisations with dates and statuses, indication mapping, edges |
257 +| `seer-explorer` | NCI SEER | epidemiology | approved | explorer datasets (no schedule) |
258 +| `seer` | NCI SEER API | epidemiology | awaiting credentials | survival and rates once `SEER_API_KEY` is set |
259 +| `iarc-globocan` | IARC | epidemiology | **review** | global burden — gated by `IARC_TERMS_ACCEPTED_BY`; nothing is ingested without a human decision |
260 +
261 +Run order for a fresh database: `deploy/first-run.sh` (terminology → genes → evidence/genomics/variants
262 +→ trials → literature → epidemiology), then the regulatory connectors, `pnpm cix reconcile-drugs`,
263 +`pnpm cix counters`, `pnpm cix intel`, `pnpm cix rank`.
264 +
265 +## Derived intelligence layer
266 +
267 +Recomputed daily by the worker (`maintenance.intel`, between counters and rankings) or with
268 +`pnpm cix intel` (≈ 50 s on the production database). Every row stores `formula_version` and `inputs`.
269 +Methods: `docs/methodology/*.md` and `/methodology`.
270 +
271 +| Module | Table | Formula version | Highlights |
272 +|---|---|---|---|
273 +| Clinical trial intelligence | `trial_intelligence` | `ci-trial-intel-v1`, stop reasons `ci-stop-reasons-v1` | per cancer (top and all levels, descendants included): total / active / recruiting / Phase I–IV counts, growth of first-posted studies (12 m vs prior 12 m, ≥ 20), enrollment mean/median, sponsor and country HHI, industry and US shares, termination share (≥ 2010, ≥ 30 terminal), stop-reason breakdown by explicit keyword rules, trials per 1 000 deaths / per 100 k cases (US, latest year, deaths ≥ 100) |
274 +| Trial map | `trial_site_country_counts` | `ci-trial-sites-v1` | sites and studies per country × (all / each top-level cancer) × (any / each phase) × (all / recruiting only); ISO 3166-1 alpha-3 mapping with explicit unmapped historical names |
275 +| Drug pipeline | `drug_pipeline`, `entity_merges` | `ci-drug-pipeline-v1` | stage per drug and per drug × top-level cancer (approved › withdrawn › highest registry phase › phase not stated); salt-form duplicates proposed to the merge queue |
276 +| Research Gap Index | `research_gap_components` + 4 ranking metrics | `ci-research-gap-components-v1` | for each burden scope: death share vs active-trial and publication shares over the eligible set (deaths ≥ 100), `log₂` gap ratios, per-1 000-deaths intensities |
277 +| Trial → drug reconciliation | `trial_interventions.drug_id` | (in the ClinicalTrials connector) | exact alias › salt/dose/label-stripped alias › probabilistic head; shared aliases resolved by rule (own generic name › brand › base molecule) or left unresolved — 78 956 rows linked |
278 +
279 +## Rankings and metrics
280 +
281 +25 metric definitions live in `metric_definitions` (seeded from `packages/database/src/seed-data/metrics.ts`)
282 +and are rendered live on `/methodology#metrics`. A ranking snapshot is one metric × one scope
283 +(`geo=USA|sex=all|age=all|year=2024|level=top`) × one formula version; rows keep rank, percentile,
284 +confidence, previous rank and the exact inputs.
285 +
286 +| Category | Metrics |
287 +|---|---|
288 +| Burden | `incidence_count`, `mortality_count`, `as_incidence_rate`, `as_mortality_rate` |
289 +| Lethality | `mortality_incidence_ratio`, `five_year_survival` (awaits survival observations) |
290 +| Clinical research | `active_trials`, `recruiting_trials`, `phase3_trials`, `phase3_recruiting_trials`, `trial_termination_share`, `sponsor_concentration` |
291 +| Research activity & trends | `publications_5y`, `publications_12m`, `publication_growth`, `trial_growth_yoy` |
292 +| Molecular knowledge | `curated_evidence_items`, `associated_genes`, `genomic_cohorts` |
293 +| Unmet need | `trial_gap`, `research_gap` (percentile-based), `trial_gap_ratio`, `research_gap_ratio` (share-based, log₂), `trials_per_1000_deaths`, `publications_per_1000_deaths` |
294 +
295 +Burden, lethality and gap metrics exist only for scopes with licensed observations (currently the
296 +United States, per year and sex, top level). No composite score is published (ADR-006).
297 +
298 +## Public API
299 +
300 +Base URL `https://www.cancerindex.io/api/v1` (proxied to Fastify). Read-only JSON. Every response is an
301 +envelope `{ data, sources, dataRelease, generatedAt, total?, limit?, offset?, hasMore? }` where `sources`
302 +lists the upstream sources, licences and attributions behind the returned data. Swagger UI at `/api/v1/docs`,
303 +OpenAPI 3.1 at `/api/v1/openapi.json`. Rate limits per IP; an optional bearer API key raises them.
304 +
305 +| Endpoints | Purpose |
306 +|---|---|
307 +| `/cancers`, `/cancers/:id`, `/cancers/:id/{statistics,survival,genes,variants,drugs,trials,publications}` | taxonomy, observations, evidence, approvals, trials per cancer (descendants included) |
308 +| `/genes`, `/genes/:symbol`, `/variants/:id`, `/drugs`, `/drugs/:id`, `/biomarkers`, `/biomarkers/:slug` | entity details with derived links |
309 +| `/trials`, `/trials/:nct`, `/trials/intelligence[/:cancer]`, `/trials/terminated`, `/trials/sites` | search, study record, per-cancer intelligence, failures with classified reasons, country/city site aggregates |
310 +| `/epidemiology`, `/epidemiology/coverage`, `/epidemiology/metrics` | time-aware observations with provenance (Data explorer) |
311 +| `/approvals`, `/approvals/recent`, `/pipeline`, `/pipeline/summary` | jurisdiction-aware regulatory records and development stages |
312 +| `/research-gap`, `/research-gap/scopes` | gap components per burden scope |
313 +| `/graph/:type/:id`, `/graph/cancer/:id/paths` | knowledge-graph neighbourhoods and chains |
314 +| `/rankings/metrics`, `/rankings`, `/rankings/:metric/:cancerId/explain` | catalogue, snapshots, "Why this rank?" |
315 +| `/search`, `/sources`, `/sources/:slug`, `/stats`, `/changes`, `/healthz` | search, registry and health, live counts, change events |
316 +| `/admin/*` (`x-admin-token`) | connectors, runs, unresolved labels, jobs, trace, audit |
317 +
318 +Details and examples: `docs/API.md`.
319 +
320 +## Operator CLI
20 321
21 322 ```
22 −apps/web Next.js 16 public site (port 8250), proxies /api/v1/* to the API
23 −apps/api Fastify 5 public API /v1 (port 8251), OpenAPI at /v1/docs
24 −workers/ pg-boss scheduler: connector cron, counters, rankings, health probes
25 −packages/ shared · database (Drizzle) · ontology · connectors (SDK + connectors) · ranking
26 −scripts/ci.ts operator CLI (pnpm cix …)
27 −deploy/ MacLustr mld manifest, first-run bootstrap
28 −docs/ architecture, data model, methodology, API, security, AI policy, ADRs, connector docs
323 +pnpm cix connectors list connectors, licence status, health, last success
324 +pnpm cix run <id> [--mode full|incremental|backfill|dry_run] [--max-records N] [--max-minutes M] [--reset-cursor]
325 +pnpm cix run-all [--max-minutes M] every active connector in registry order
326 +pnpm cix health <id> source liveness probe
327 +pnpm cix sources:sync manifests → sources table
328 +pnpm cix reconcile-drugs [--remap] trial interventions → canonical drugs
329 +pnpm cix counters rebuild entity_counters
330 +pnpm cix intel trial intelligence · trial sites · drug pipeline · research gap
331 +pnpm cix rank recompute every ranking snapshot
332 +pnpm cix stats | trace <type> <id> | doctor [--no-disk] | alerts [ack|resolve <id>]
29 333 ```
30 334
31 −Start with `CLAUDE.md` (operational rules), then `docs/ARCHITECTURE.md`, `docs/DATA-MODEL.md` and
32 −`docs/METHODOLOGY.md`.
335 +`--mode backfill` replays a connector's raw lake without HTTP (used after a mapping-rule change); `dry_run`
336 +never writes and never moves a cursor.
33 337
34 −## Quickstart
338 +## Local development
35 339
36 −Requirements: Node ≥ 22, pnpm 11, PostgreSQL 17 with `pg_trgm`, `unaccent` and `vector`.
340 +Requirements: Node ≥ 22, pnpm 11, PostgreSQL 17 with `pg_trgm`, `unaccent` and `vector`; `rsvg-convert`
341 +and Python Pillow only if you regenerate brand rasters.
37 342
38 343 ```bash
39 −createdb cancerindex && cp .env.example .env # set NCBI_EMAIL, ADMIN_TOKEN
344 +createdb cancerindex && cp .env.example .env # set NCBI_EMAIL, ADMIN_TOKEN
40 345 pnpm install
41 346 pnpm db:migrate && pnpm db:seed && pnpm cix sources:sync
42 −pnpm cix run oncotree --mode dry_run # smoke test against the live API (no writes)
43 −pnpm cix run oncotree # first real ingest
44 −pnpm cix counters && pnpm cix rank
45 −pnpm dev:api & # http://127.0.0.1:8251/v1/docs
46 −pnpm worker & # schedules from connector manifests
47 −pnpm dev:web # http://localhost:8250
347 +pnpm cix run oncotree --mode dry_run # smoke against the live API, no writes
348 +bash deploy/first-run.sh # ordered first ingestion (hours; restartable)
349 +pnpm cix run health-canada-dpd && pnpm cix run ema && pnpm cix run openfda
350 +pnpm cix reconcile-drugs && pnpm cix counters && pnpm cix intel && pnpm cix rank
351 +pnpm dev:api & # http://127.0.0.1:8251/v1/docs
352 +pnpm worker & # schedules from connector manifests
353 +pnpm dev:web # http://localhost:8250
48 354 ```
49 355
50 −Verify: `curl -s localhost:8251/healthz`, `curl -s "localhost:8251/v1/search?q=glio"`.
356 +A faster path for UI work is to restore a production dump (`bash deploy/restore.sh <dump> --target
357 +cancerindex_prodcopy`, then rename databases) and run `pnpm db:migrate && pnpm db:seed`.
358 +Note that Next.js allows a single `next dev` per app directory (`.next/dev/lock`).
51 359
52 −## CLI
360 +## Configuration
53 361
54 −```
55 −pnpm cix connectors list connectors, license status, health
56 −pnpm cix run <id> [--mode full|incremental|dry_run] [--max-records N] [--max-minutes M] [--reset-cursor]
57 −pnpm cix run-all [--max-minutes M] every active connector in registry order
58 −pnpm cix health <id> source liveness probe
59 −pnpm cix sources:sync manifests → sources table
60 −pnpm cix counters rebuild entity_counters
61 −pnpm cix rank recompute ranking snapshots
62 −pnpm cix stats table counts
63 −pnpm cix trace <table> <id> lineage of a value
64 −
65 −tsx workers/cli.ts run <id> | counters [--then-rank] | rank | health | schedules | queues
66 −pnpm --filter @cancerindex/api create-key -- --label "Lab" --tier research --rpm 600
67 −```
362 +`.env` at the repository root (read by the API, the worker and the web app).
363 +
364 +| Variable | Purpose |
365 +|---|---|
366 +| `DATABASE_URL`, `DB_POOL_MAX` | PostgreSQL connection |
367 +| `WEB_PORT` (8250), `API_PORT` (8251), `API_HOST`, `CI_API_URL` | ports and the API URL the web app proxies to |
368 +| `NEXT_PUBLIC_SITE_URL` | canonical site URL (metadata, sitemaps, share images) |
369 +| `CI_DATA_DIR` | raw data lake root (`data/raw`, `data/cache`) |
370 +| `ADMIN_TOKEN` | `/admin` console and `/v1/admin/*` |
371 +| `NCBI_TOOL`, `NCBI_EMAIL`, `NCBI_API_KEY` | E-utilities (PubMed, ClinVar); the key raises the limit to 10 req/s |
372 +| `SEER_API_KEY` | unlocks the SEER connector |
373 +| `OPENFDA_API_KEY` | raises openFDA from 1 000 to 120 000 requests/day |
374 +| `IARC_TERMS_ACCEPTED_BY` | human gate for GLOBOCAN (stays unset until terms are settled) |
375 +| `WORKER_CONCURRENCY`, `CI_MAX_RUN_MINUTES`, `LOG_LEVEL` | worker tuning |
376 +| `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL` | reserved for the future `/ask` layer (not enabled) |
377 +
378 +## Quality: tests, QA, accessibility, performance
68 379
69 −## Quality gates
380 +- `pnpm typecheck` — strict TypeScript across the 8 workspaces.
381 +- `pnpm test` — 528 vitest tests: connector fixtures (every connector), SDK (lake, run, checkpoints),
382 + formulas (shares, log ratios, HHI, growth, termination share, stop-reason rules, pipeline stages,
383 + country codes, map scales, projections, explorer comparability, CSV escaping, graph layout), API
384 + smoke tests against the local database (skipped when unreachable).
385 +- `pnpm --filter @cancerindex/web qa` / `qa:prod` — read-only smoke suite over 44 routes: HTTP 200,
386 + expected text, per-route weight caps, no "Data not yet available" where data must exist, no horizontal
387 + overflow at 390 px, console-error scan (Playwright when available).
388 +- Accessibility: keyboard navigation, focus rings, colour never the only carrier (badges carry text,
389 + charts have legends and data tables, maps have equivalent tables), `aria` labels on SVG figures.
390 +- Performance: ISR caching (`revalidate` per page, `Cache-Control` on public routes), materialized
391 + derived tables, GIN / trigram indexes, `sql.param` for array parameters, request budgets on heavy
392 + pages (graph groups, city layers).
393 +
394 +## Deployment and operations
395 +
396 +Production runs on the MacLustr cluster (node **M4M64b**: Postgres 17 + pgvector, PM2) behind the
397 +MacLustr Tunnel (WireGuard + Caddy on the BHS64 gateway) at https://www.cancerindex.io.
70 398
71 399 ```bash
72 −pnpm typecheck # every workspace
73 −pnpm -r test # vitest; connector tests use fixtures, API smoke tests skip without a DB
400 +# from the laptop — gateway M1M32 orchestrates (mld)
401 +rsync -a --exclude '/data/' --exclude node_modules --exclude .next --exclude .git --exclude '.env*' ./ /tmp/cancerindex-deploy/
402 +mld stage /tmp/cancerindex-deploy cancerindex
403 +mld deploy cancerindex --node M4M64b # install, migrate, seed, sources:sync, build, PM2 restart, health checks
74 404 ```
75 405
76 −## Deployment
406 +| Process (PM2) | Role |
407 +|---|---|
408 +| `cancerindex-web` (:8250) | `next start` |
409 +| `cancerindex-api` (:8251) | Fastify API |
410 +| `cancerindex-worker` | pg-boss: connector schedules, `maintenance.counters` 06:00 UTC → `maintenance.intel` 06:15 → `maintenance.rank` 06:30, hourly health probes and alerts |
411 +| `cancerindex-backup` | nightly `pg_dump` 05:20, 14 daily + 8 weekly retained (`deploy/backup.sh`, `restore.sh`) |
412 +
413 +Operational visibility: `/data-updates` (connector state, ingestion log), `/admin` (runs, unresolved
414 +labels, alerts), `pnpm cix doctor`, `system_alerts`. Deploy manifest: `deploy/mld-manifest.cancerindex.json`
415 +(note the excludes are anchored — `/coverage/`, `/data/` — so route folders with those names are kept).
416 +
417 +## Brand
418 +
419 +<p>
420 + <img src="apps/web/public/brand/logo-mark.svg" alt="CancerIndex mark" width="72" align="left" hspace="12">
421 + The mark reads <strong>CI</strong>: an open ring — a cell with its nucleus, the "C" — and a graduated
422 + index scale in the accent teal, the "I". It is drawn once in SVG and used everywhere: the header and
423 + footer (<code>BrandMark</code>, theme-aware through <code>currentColor</code> and the accent token),
424 + the favicon (<code>app/icon.svg</code>, <code>favicon.ico</code>, <code>apple-icon.png</code>), the web
425 + manifest and the share images.
426 +</p>
427 +<br clear="all">
428 +
429 +| Asset | Path |
430 +|---|---|
431 +| Logo with wordmark | `apps/web/public/brand/logo.svg` |
432 +| Mark (light / dark backgrounds) | `apps/web/public/brand/logo-mark.svg`, `logo-mark-dark.svg` (+ PNG 512) |
433 +| Favicon and app icons | `apps/web/src/app/icon.svg`, `favicon.ico`, `apple-icon.png`, `public/brand/icon-{192,512}.png` |
434 +| Share images | `apps/web/src/lib/og.tsx` (next/og, 1200 × 630, site fonts); `app/opengraph-image.tsx` (live counts) and `opengraph-image.tsx` under `cancer/[slug]`, `gene/[symbol]`, `drug/[slug]`, `biomarker/[slug]`, `trial/[nct]` |
435 +
436 +Colours: paper `#fafaf7`, ink `#1c1c1a`, accent teal `#0f5f63` (dark theme `#6cc3c6`). Type: Newsreader
437 +(display), Inter (UI), IBM Plex Mono (identifiers).
438 +
439 +## Design language
440 +
441 +Scientific, editorial, institutional. Off-white paper, charcoal ink, one restrained teal accent, thin
442 +rules, tabular numerals, dense tables, sparklines and small multiples rather than decorative charts. All
443 +charts are server-rendered SVG with a legend, units, the dataset year and a source line; every value shows
444 +a claim badge (observed · published · curated · regulatory · computed) and a source badge whose popover
445 +gives dataset, version, retrieval date and URL. Mobile is first-class (tables scroll inside their wrap; no
446 +horizontal overflow at 390 px). Light and dark themes share one token set (`--color-*`, `--color-series-*`).
447 +
448 +## Known gotchas
449 +
450 +- A JS array inside a Drizzle template — `` sql`x = ANY(${arr}::text[])` `` — becomes a `($1,$2,…)` tuple
451 + and fails; use `sql.param(arr)` or `sql.raw` for constant lists.
452 +- Columns declared with the `updatedAt()` helper are physically `updated_at` even when named `computedAt`.
453 +- `db.execute<T>` needs a `type` alias, not an `interface`, for `T`.
454 +- A prop named `ref` cannot be passed to a server component (React reserves it) — the page fails in
455 + production with minified error #441; use another name (`entityRef`).
456 +- SVG `<title>` must be a single text node or hydration fails; `array_agg` over empty arrays errors — use
457 + `unnest` / `string_agg`.
458 +- `opengraph-image.tsx` cannot live inside an optional catch-all segment (`[[...tab]]`); place it one
459 + level up.
460 +- `ranking_snapshots.scope_key` has no source dimension: two sources for the same geography/year/sex
461 + overwrite each other's current snapshot, so a preferred source is chosen per scope.
462 +- A shared alias between a concept and its descendant ("breast cancer") must resolve to the **broadest**
463 + concept when building a dictionary; the narrowest-of-lineage rule is only for sentence-level mentions.
464 +- OncoTree's WAF rejects any User-Agent containing a URL; NCBI throttles at 3 req/s without a key;
465 + openFDA allows 1 000 requests/day without a key; the EMA xlsx has 39 columns and header row 9.
466 +- mld `sync_excludes` patterns must be anchored (`/data/`, `/coverage/`), otherwise route folders with
467 + the same name are dropped from the deploy.
77 468
78 −Production runs on the MacLustr cluster through `mld` (gateway M1M32): manifest
79 −`deploy/mld-manifest.cancerindex.json`, `mld stage <dir> cancerindex`, `mld deploy cancerindex`,
80 −then `deploy/first-run.sh` for the ordered first ingestion. PM2 processes `cancerindex-web`,
81 −`cancerindex-api`, `cancerindex-worker`; ngrok `www.cancerindex.io` → 8250. See `deploy/README.md`.
469 +## Roadmap
82 470
83 −## Principles (short form)
471 +- Global burden once IARC / GLOBOCAN terms are settled (human decision) and SEER credentials are set;
472 + survival observations and the `five_year_survival` metric follow.
473 +- MHRA and TGA regulatory connectors; populating `drug_approvals.biomarker_ids` from label text.
474 +- Risk factors and attributable burden, screening and guideline registries (metadata and links only,
475 + with change detection), hereditary syndromes, pediatric and rare-cancer views.
476 +- Researcher, institution and funding entities (OpenAlex, NIH RePORTER, CIHR) for the funding-gap view.
477 +- Watchlists, alerts and a grounded, cited `/ask` layer that summarizes indexed sources only.
478 +- Lighter default views for the heaviest pages (`/graph`, biomarker pages with hundreds of approvals).
84 479
85 −1. No scientific number without provenance; derived values carry a formula version and inputs.
86 −2. Layers stay separable: RAW → NORMALIZED → CANONICAL → DERIVED → RANKED → AI.
87 −3. Never fake data: missing is missing ("Data not yet available"), never zero.
88 −4. Identifiers are first-class: every upstream id is kept; public ids are `CI-<NS>-00000001`.
89 −5. Reconciliation before ingestion; unknown labels go to a curation queue, never dropped.
90 −6. Time-aware observations: a new year is a new row.
91 −7. Every edge has context: cancer, direction, evidence level, provenance.
92 −8. Licensing gate on every source; scientific safety labels are never merged.
480 +## Licensing, attribution and contact
93 481
94 −## License and attribution
482 +- **Code**: private repository (spbgit `cancerindex.git`).
483 +- **Derived data** (rankings, intelligence tables, CSV exports): © CancerIndex, CC BY 4.0, with
484 + attribution rows naming the underlying providers.
485 +- **Source data** remains under the licence of each provider — see `/sources` and the `sources` array of
486 + every API response (NCIt CC BY 4.0, OncoTree CC BY 4.0, HGNC CC0, CIViC CC0, openFDA CC0, Health Canada
487 + OGL, EMA with acknowledgement, ClinVar / PubMed / ClinicalTrials.gov / CDC public domain, GDC open tier,
488 + ChEMBL CC BY-SA).
489 +- **Not medical advice.** CancerIndex provides research and educational information only.
95 490
96 −Code: private (Groupe/SPB). Data: each source keeps its own license and attribution, surfaced in
97 −every API response (`sources`) and on `/sources`. See `docs/source-policy.md`.
491 +Built and maintained by **Simon-Pierre Boucher** — contact **contact@spboucher.ai** — hosted on
492 +**MacLustr** (<https://www.maclustr.io>).
98 493