SPB Git

spb/cancerindex

Public

Foundation: monorepo, schema, provenance SDK, ontology resolver, ranking engine, OncoTree connector

Simon-Pierre Boucher committed (Sep 8, 2026) Browse files

74 changed files +13,851 −0

74 changed files
Unified Split
added .env.example +34 −0
@@ -0,0 +1,34 @@
1+# CancerIndex — environment (never commit real values)
2+NODE_ENV=development
3+DATABASE_URL=postgres://localhost:5432/cancerindex
4+DB_POOL_MAX=10
5+
6+WEB_PORT=8250
7+API_PORT=8251
8+API_HOST=127.0.0.1
9+CI_API_URL=http://127.0.0.1:8251
10+NEXT_PUBLIC_SITE_URL=http://localhost:8250
11+NEXT_TELEMETRY_DISABLED=1
12+
13+# Data lake (raw connector payloads, gzip JSON) — outside git
14+CI_DATA_DIR=./data
15+
16+# Admin console (/admin) bearer token
17+ADMIN_TOKEN=change-me
18+
19+# NCBI E-utilities (PubMed, ClinVar) — tool/email are mandatory, api key raises the limit to 10 req/s
20+NCBI_TOOL=cancerindex
21+NCBI_EMAIL=
22+NCBI_API_KEY=
23+
24+# SEER API (free key from https://api.seer.cancer.gov/) — connector stays "awaiting_credentials" without it
25+SEER_API_KEY=
26+
27+# Optional AI provider for /ask (Phase 5) — the database works without it
28+ANTHROPIC_API_KEY=
29+OPENAI_API_KEY=
30+OPENAI_BASE_URL=
31+
32+WORKER_CONCURRENCY=4
33+CI_MAX_RUN_MINUTES=45
34+LOG_LEVEL=info
added .gitignore +15 −0
@@ -0,0 +1,15 @@
1+node_modules/
2+.next/
3+dist/
4+*.tsbuildinfo
5+.env
6+.env.*
7+!.env.example
8+data/raw/
9+data/cache/
10+data/tmp/
11+logs/
12+tmp/
13+coverage/
14+.DS_Store
15+.claude/
added .npmrc +2 −0
@@ -0,0 +1,2 @@
1+auto-install-peers=true
2+strict-peer-dependencies=false
added CLAUDE.md +82 −0
@@ -0,0 +1,82 @@
1+# CLAUDE.md — CancerIndex.io (repository guide)
2+
3+The full product specification (364 sections) lives in `docs/SPEC-original.md`. This file is the
4+condensed, operational version every contributor (human or agent) must follow.
5+
6+## Mission
7+Build **the global index of cancer**: a provenance-first, continuously updated, transparently
8+sourced oncology ontology connecting epidemiology, genomics, biomarkers, therapies, trials,
9+regulatory evidence and literature — for every recognized malignant disease entity, not a list of
10+30 common cancers. Not a physician, not a diagnostic tool, no treatment recommendations.
11+
12+## Non-negotiables (from the spec)
13+1. **Provenance first** — no scientific number without a `provenance` row; derived values carry a
14+ `formula_version` and their inputs (§2, §250-253). Raw payloads go to the data lake (§26).
15+2. **Layers stay separable**: RAW → NORMALIZED → CANONICAL → DERIVED → RANKED → AI (§2).
16+3. **Never fake data**: no mock/hardcoded statistics in UI; show "Data not yet available" (§281-283).
17+4. **Identifiers are first-class**: keep every upstream ID in `*_codes` / dedicated columns (§347);
18+ public IDs are `CI-<NS>-00000001` minted via `mintId` (§6), never DB integers.
19+5. **Reconciliation before ingestion**: IDs → curated aliases → normalized strings; LLM only as a
20+ *candidate* generator; unknown labels go to `unresolved_labels`, never dropped (§69, §222, §340).
21+6. **Time-aware observations**: never overwrite year X with year Y; store both (§71).
22+7. **Context on every edge**: cancer context, direction, evidence level, provenance (§29, §244).
23+8. **Licensing gate**: no connector goes live before manifest `licenseStatus` is reviewed; IARC/
24+ GLOBOCAN stays `review`, SEER `awaiting_credentials` until keys/terms are settled (§10.4, §142).
25+9. **Verify docs before coding a connector** — official docs, smoke test with curl, fixture, tests,
26+ prove on 10/100/1000 records before a full sync (§224-227). Record `documentationVerifiedAt`.
27+10. **Rate limits, idempotency, restartability, anomaly guard** (never mass-delete on a shrunken
28+ response) (§90-92, §171, §229).
29+11. **Rankings need scope + year + source + formula version + "Why this rank?"** (§33, §183).
30+ Composite scores are Phase 2+ (§353).
31+12. **Scientific safety labels**: observed / published / curated / regulatory / guideline /
32+ computed / AI-generated — never merged (§3). Population survival ≠ individual prognosis (§325).
33+
34+## Repository layout
35+```
36+apps/web Next.js 16 (webpack build, --webpack), Tailwind v4, server components, /api/v1 proxied to apps/api
37+apps/api Fastify /v1 public API (JSON envelope { data, sources, dataRelease })
38+workers/ pg-boss scheduler: connector schedules, counters, rankings
39+packages/shared ids, provenance types, normalization, logger
40+packages/database Drizzle schema (snake_case casing), migrations, seed (metrics + geographies only)
41+packages/ontology qualifier rules, CancerResolver (alias/code reconciliation), TOP_LEVEL_CANCERS
42+packages/connectors SDK (manifest, HttpClient, RawLake, RunContext, validators) + connectors/<id>/{manifest.ts,index.ts,fixtures/,*.test.ts}
43+packages/ranking counters (entity_counters), ranking engine (snapshots + lineage), trace
44+scripts/ci.ts operator CLI: `pnpm cix connectors|run <id>|run-all|health|sources:sync|counters|rank|stats|trace`
45+docs/ architecture, data model, methodology, source policy, ADRs, connector docs
46+data/raw data lake (gitignored) — {source}/{date}/{entity}/{runId}-{part}.jsonl.gz
47+```
48+
49+## Conventions
50+- TypeScript strict, ESM, NodeNext imports with `.js` suffix inside packages; web app uses
51+ `moduleResolution: bundler` + webpack `extensionAlias` (never import `@cancerindex/connectors` in web).
52+- Database: `getDb()` singleton; Drizzle `casing: 'snake_case'`; raw SQL via `sql` template for
53+ aggregates. Schema changes = new migration via `pnpm db:generate` (one per integration), never
54+ manual prod mutation. Agents developing in parallel use `drizzle-kit push` on their own DB and do
55+ **not** commit migration files; the integrator generates the migration.
56+- Connector = `class extends Connector` with `manifest`, `healthCheck(ctx)`, `sync(ctx)`; register
57+ in `packages/connectors/src/registry.ts`. Use `ctx.http` (rate-limited), `ctx.upsertSourceRecord`
58+ (idempotent + raw lake), `ctx.addProvenance`, `ctx.recordUnresolved`, `ctx.cursor` (restart),
59+ `ctx.shouldStop()` between pages (time budget), validators from `sdk/validate.ts`.
60+- Reconciliation: `new CancerResolver(db)` → `warm()` → `byCode()/byLabel()/resolve()`; store
61+ `match_type` on every mapping; enqueue misses with `ctx.recordUnresolved`.
62+- Tests: vitest; connector tests run against **fixtures** (sanitized JSON in `fixtures/`), never
63+ live APIs; cover normal / empty / pagination / rate-limit / server error / malformed record.
64+- UI: scientific-editorial aesthetic (off-white, charcoal, muted neutrals, dense tables, no
65+ gradients/cards-everywhere), WCAG-minded, mobile first-class. Every number shows source badge,
66+ unit, population, period and a freshness line. Empty state = "Data not yet available".
67+- Language: English UI; code comments English; commit messages English.
68+
69+## Running locally
70+```
71+createdb cancerindex && cp .env.example .env
72+pnpm install && pnpm db:migrate && pnpm db:seed && pnpm cix sources:sync
73+pnpm cix run oncotree --mode dry_run # smoke; then without --mode
74+pnpm cix counters && pnpm cix rank
75+pnpm dev:api & pnpm dev:web
76+```
77+
78+## Deployment
79+MacLustr cluster via `mld` (gateway M1M32): manifest `~/dispatch/apps/cancerindex.json`,
80+`mld stage <dir> cancerindex` then `mld deploy cancerindex`. Web :8250, API :8251, PM2 processes
81+`cancerindex-web|api|worker|ngrok`, domain www.cancerindex.io (ngrok reserved). Postgres 17 +
82+pgvector on the node, database `cancerindex`. Source of truth for the code: spbgit `cancerindex.git`.
added package.json +40 −0
@@ -0,0 +1,40 @@
1+{
2+ "name": "cancerindex",
3+ "version": "0.1.0",
4+ "private": true,
5+ "description": "CancerIndex.io — the global index of cancer: provenance-first oncology ontology, epidemiology, genomics, trials, literature, rankings",
6+ "type": "module",
7+ "packageManager": "pnpm@11.1.2",
8+ "engines": {
9+ "node": ">=22"
10+ },
11+ "scripts": {
12+ "build": "pnpm -r --filter './packages/**' run build && pnpm --filter @cancerindex/api run build && pnpm --filter @cancerindex/web run build",
13+ "typecheck": "pnpm -r run typecheck",
14+ "test": "pnpm -r run test",
15+ "db:generate": "pnpm --filter @cancerindex/database run generate",
16+ "db:migrate": "pnpm --filter @cancerindex/database run migrate",
17+ "db:seed": "pnpm --filter @cancerindex/database run seed",
18+ "dev:web": "pnpm --filter @cancerindex/web run dev",
19+ "dev:api": "pnpm --filter @cancerindex/api run dev",
20+ "worker": "tsx workers/main.ts",
21+ "cix": "tsx scripts/ci.ts",
22+ "registry": "tsx scripts/build-registry.ts",
23+ "registry:check": "tsx scripts/build-registry.ts --check"
24+ },
25+ "dependencies": {
26+ "@cancerindex/connectors": "workspace:*",
27+ "@cancerindex/database": "workspace:*",
28+ "@cancerindex/ontology": "workspace:*",
29+ "@cancerindex/ranking": "workspace:*",
30+ "@cancerindex/shared": "workspace:*",
31+ "drizzle-orm": "^0.45.0",
32+ "postgres": "^3.4.7"
33+ },
34+ "devDependencies": {
35+ "@types/node": "^24.0.0",
36+ "tsx": "^4.20.0",
37+ "typescript": "^5.9.3",
38+ "vitest": "^3.2.0"
39+ }
40+}
added packages/connectors/package.json +36 −0
@@ -0,0 +1,36 @@
1+{
2+ "name": "@cancerindex/connectors",
3+ "version": "0.1.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": {
8+ "types": "./src/index.ts",
9+ "default": "./src/index.ts"
10+ },
11+ "./sdk": {
12+ "types": "./src/sdk/index.ts",
13+ "default": "./src/sdk/index.ts"
14+ }
15+ },
16+ "scripts": {
17+ "build": "tsc -p tsconfig.json --noEmit",
18+ "typecheck": "tsc -p tsconfig.json --noEmit",
19+ "test": "vitest run --passWithNoTests"
20+ },
21+ "dependencies": {
22+ "@cancerindex/database": "workspace:*",
23+ "@cancerindex/ontology": "workspace:*",
24+ "@cancerindex/shared": "workspace:*",
25+ "drizzle-orm": "^0.45.0",
26+ "fast-xml-parser": "^5.2.0",
27+ "postgres": "^3.4.7",
28+ "yaml": "^2.8.0",
29+ "zod": "^4.0.0"
30+ },
31+ "devDependencies": {
32+ "@types/node": "^24.0.0",
33+ "typescript": "^5.9.3",
34+ "vitest": "^3.2.0"
35+ }
36+}
added packages/connectors/src/connectors/oncotree/index.ts +185 −0
@@ -0,0 +1,185 @@
1+import { eq, sql } from 'drizzle-orm';
2+import { z } from 'zod';
3+import { normalizeLabel, slugify } from '@cancerindex/shared';
4+import { anatomicalSites, cancerAliases, cancerAnatomy, cancerCodes, cancerHierarchy, cancers, mintId } from '@cancerindex/database';
5+import { CancerResolver } from '@cancerindex/ontology';
6+import { Connector, type ConnectorHealth, type RunContext } from '../../sdk/run.js';
7+import { manifest } from './manifest.js';
8+
9+const TumorType = z.object({
10+ code: z.string(),
11+ name: z.string(),
12+ mainType: z.string().nullable().optional(),
13+ tissue: z.string().nullable().optional(),
14+ parent: z.string().nullable().optional(),
15+ level: z.number().int(),
16+ externalReferences: z.record(z.string(), z.array(z.string())).default({}),
17+ history: z.array(z.string()).default([]),
18+ revocations: z.array(z.string()).default([]),
19+ precursors: z.array(z.string()).default([]),
20+ color: z.string().nullable().optional(),
21+});
22+type TumorType = z.infer<typeof TumorType>;
23+
24+const HEME_RE = /lymphoid|myeloid|blood|leukemia|lymphoma|myeloma|histiocyt|mastocyt|plasma cell/i;
25+const BENIGN_RE = /\bbenign\b|borderline|\bnevus\b|\bnevi\b|\badenoma\b|hyperplasia|\bcyst\b|hamartoma|\blipoma\b|leiomyoma\b|\bfibroma\b|hemangioma\b|papilloma\b|dysplasia|in situ|\bpolyp\b|schwannoma\b|meningioma\b|paraganglioma\b|pheochromocytoma\b|teratoma\b|thymoma\b/i;
26+
27+/**
28+ * OncoTree connector: maps every OncoTree node onto the canonical cancer set.
29+ * - level-1 "tissue" nodes (parent TISSUE) are anatomical sites, not cancers → anatomical_sites
30+ * - NCIt cross-reference → EXACT_IDENTIFIER match to the NCIt-derived canonical entity
31+ * - otherwise preferred-name match → ONTOLOGY_EXACT
32+ * - otherwise a new canonical entity is created *only* because OncoTree is a recognized ontology
33+ * (CLAUDE.md §340) — flagged with primaryOncotreeCode and no NCIt code.
34+ * The OncoTree parent/child structure is stored as hierarchy_type = "oncotree" (levels ≥ 2), and
35+ * level-2 nodes are linked to their tissue via cancer_anatomy.
36+ */
37+export class OncotreeConnector extends Connector {
38+ readonly manifest = manifest;
39+
40+ async healthCheck(ctx: RunContext): Promise<ConnectorHealth> {
41+ try {
42+ const versions = await ctx.http.json<Array<{ api_identifier: string; release_date: string }>>(`${manifest.access.baseUrl}/versions`);
43+ const stable = versions.find((v) => v.api_identifier === 'oncotree_latest_stable');
44+ return { status: 'healthy', detail: `latest stable ${stable?.release_date ?? '?'}` };
45+ } catch (e) {
46+ return { status: 'failing', detail: (e as Error).message };
47+ }
48+ }
49+
50+ async sync(ctx: RunContext): Promise<void> {
51+ const versions = await ctx.http.json<Array<{ api_identifier: string; release_date: string; visible: boolean }>>(`${manifest.access.baseUrl}/versions`);
52+ const stable = versions.find((v) => v.api_identifier === 'oncotree_latest_stable');
53+ // Pin to the dated release identifier so the dataset version is reproducible.
54+ const dated = versions.find((v) => v.release_date === stable?.release_date && v.api_identifier !== 'oncotree_latest_stable');
55+ const version = dated?.api_identifier ?? 'oncotree_latest_stable';
56+ ctx.datasetVersion = version;
57+ ctx.info(`OncoTree version ${version}`);
58+
59+ const raw = await ctx.http.json<unknown[]>(`${manifest.access.baseUrl}/tumorTypes?version=${encodeURIComponent(version)}`);
60+ const nodes: TumorType[] = [];
61+ for (const r of raw) {
62+ const parsed = TumorType.safeParse(r);
63+ if (!parsed.success) {
64+ ctx.counters.validationFailures++;
65+ ctx.counters.rejected++;
66+ ctx.warn(`invalid tumor type record: ${parsed.error.issues[0]?.message}`);
67+ continue;
68+ }
69+ nodes.push(parsed.data);
70+ }
71+ ctx.info(`fetched ${nodes.length} tumor types`);
72+ if (nodes.length < 500) throw new Error(`anomaly: only ${nodes.length} OncoTree nodes returned (expected ~900) — refusing to persist (CLAUDE.md §171)`);
73+ if (ctx.mode === 'dry_run') {
74+ ctx.counters.fetched = nodes.length;
75+ return;
76+ }
77+
78+ const resolver = new CancerResolver(ctx.db);
79+ await resolver.warm();
80+ const byCode = new Map(nodes.map((n) => [n.code, n]));
81+ const canonicalByOncotree = new Map<string, string>();
82+ const siteByOncotree = new Map<string, string>();
83+ await ctx.addProvenance({ dataset: 'OncoTree tumor types', datasetVersion: version, sourceUrl: `${manifest.access.baseUrl}/tumorTypes?version=${version}`, evidenceType: 'expert_curation', accessLevel: 'open', methodology: 'OncoTree curated tumor type hierarchy' });
84+
85+ // Pass 0: tissue roots → anatomical sites
86+ for (const node of nodes.filter((n) => n.parent === 'TISSUE')) {
87+ await ctx.upsertSourceRecord('tissue', node.code, node);
88+ const slug = slugify(node.name);
89+ const ncit = node.externalReferences['NCI']?.[0] ?? null;
90+ const [existing] = await ctx.db.select({ id: anatomicalSites.id }).from(anatomicalSites).where(eq(anatomicalSites.slug, slug)).limit(1);
91+ let siteId = existing?.id;
92+ if (!siteId) {
93+ siteId = await mintId(ctx.db, 'ANAT');
94+ await ctx.db.insert(anatomicalSites).values({ id: siteId, name: node.name, slug, ncitCode: ncit, system: node.tissue ?? node.name });
95+ } else await ctx.db.update(anatomicalSites).set({ name: node.name, ncitCode: ncit }).where(eq(anatomicalSites.id, siteId));
96+ siteByOncotree.set(node.code, siteId);
97+ }
98+
99+ // Pass 1: resolve / create canonical entities (parents before children thanks to level ordering).
100+ for (const node of [...nodes].filter((n) => n.parent !== 'TISSUE').sort((a, b) => a.level - b.level)) {
101+ const rec = await ctx.upsertSourceRecord('tumor_type', node.code, node);
102+ let cancerId: string | null = null;
103+ let matchType = 'UNRESOLVED';
104+ const ncitCodes = node.externalReferences['NCI'] ?? [];
105+ for (const c of ncitCodes) {
106+ const hit = resolver.byCode('ncit', c);
107+ if (hit) {
108+ cancerId = hit.cancerId;
109+ matchType = hit.matchType;
110+ break;
111+ }
112+ }
113+ if (!cancerId) {
114+ const hit = resolver.byCode('oncotree', node.code);
115+ if (hit) {
116+ cancerId = hit.cancerId;
117+ matchType = hit.matchType;
118+ }
119+ }
120+ if (!cancerId) {
121+ const hit = resolver.byLabel(node.name);
122+ if (hit && hit.matchType === 'ONTOLOGY_EXACT') {
123+ cancerId = hit.cancerId;
124+ matchType = hit.matchType;
125+ }
126+ }
127+ if (!cancerId) {
128+ // Recognized ontology node without NCIt anchor → new canonical entity (CLAUDE.md §340 satisfied: OncoTree is a recognized ontology).
129+ const slugBase = slugify(node.name);
130+ let slug = slugBase;
131+ for (let i = 2; ; i++) {
132+ const [clash] = await ctx.db.select({ id: cancers.id }).from(cancers).where(eq(cancers.slug, slug)).limit(1);
133+ if (!clash) break;
134+ slug = `${slugBase}-${i}`;
135+ }
136+ const heme = HEME_RE.test(`${node.tissue ?? ''} ${node.mainType ?? ''} ${node.name}`);
137+ cancerId = await mintId(ctx.db, 'CAN');
138+ await ctx.db.insert(cancers).values({
139+ id: cancerId,
140+ slug,
141+ canonicalName: node.name,
142+ entityType: node.level >= 3 ? 'subtype' : heme ? 'hematologic_malignancy' : 'cancer',
143+ malignant: !BENIGN_RE.test(node.name),
144+ solidTumor: !heme,
145+ hematologic: heme,
146+ primaryOncotreeCode: node.code,
147+ depth: node.level,
148+ classificationVersion: version,
149+ });
150+ await ctx.db.insert(cancerAliases).values({ cancerId, alias: node.name, normalized: normalizeLabel(node.name), aliasType: 'preferred', sourceId: ctx.sourceId, sourceTerminology: 'OncoTree' }).onConflictDoNothing();
151+ matchType = 'EXACT_IDENTIFIER';
152+ await ctx.recordChange('cancer', cancerId, 'created', `Created from OncoTree ${node.code} (${node.name})`);
153+ } else {
154+ await ctx.db.insert(cancerAliases).values({ cancerId, alias: node.name, normalized: normalizeLabel(node.name), aliasType: 'synonym', sourceId: ctx.sourceId, sourceTerminology: 'OncoTree' }).onConflictDoNothing();
155+ }
156+ canonicalByOncotree.set(node.code, cancerId);
157+ await ctx.db.insert(cancerCodes).values({ cancerId, system: 'oncotree', code: node.code, matchType, sourceId: ctx.sourceId, validFrom: stable?.release_date }).onConflictDoNothing();
158+ for (const u of node.externalReferences['UMLS'] ?? []) await ctx.db.insert(cancerCodes).values({ cancerId, system: 'umls', code: u, matchType: 'EXACT_IDENTIFIER', sourceId: ctx.sourceId }).onConflictDoNothing();
159+ for (const c of ncitCodes) await ctx.db.insert(cancerCodes).values({ cancerId, system: 'ncit', code: c, matchType: 'EXACT_IDENTIFIER', sourceId: ctx.sourceId }).onConflictDoNothing();
160+ // Record the OncoTree code on the entity only when it has none yet (first mapping wins; others stay in cancer_codes).
161+ await ctx.db.execute(sql`UPDATE cancers SET primary_oncotree_code = ${node.code} WHERE id = ${cancerId} AND primary_oncotree_code IS NULL`);
162+ // Level-2 nodes hang directly under a tissue → anatomical relation.
163+ if (node.parent && siteByOncotree.has(node.parent)) {
164+ await ctx.db.insert(cancerAnatomy).values({ cancerId, siteId: siteByOncotree.get(node.parent)!, relation: 'primary', sourceId: ctx.sourceId }).onConflictDoNothing();
165+ }
166+ void rec;
167+ }
168+
169+ // Pass 2: hierarchy edges (oncotree dimension), levels ≥ 2 only.
170+ let edges = 0;
171+ for (const node of nodes) {
172+ if (!node.parent || node.parent === 'TISSUE' || siteByOncotree.has(node.parent)) continue;
173+ const childId = canonicalByOncotree.get(node.code);
174+ const parentId = canonicalByOncotree.get(node.parent);
175+ if (!childId || !parentId || childId === parentId) continue;
176+ if (!byCode.has(node.parent)) continue;
177+ await ctx.db.insert(cancerHierarchy).values({ parentId, childId, hierarchyType: 'oncotree', sourceId: ctx.sourceId }).onConflictDoNothing();
178+ edges++;
179+ }
180+ ctx.info(`tissues ${siteByOncotree.size}; canonical mapped ${canonicalByOncotree.size}; hierarchy edges ${edges}`);
181+ ctx.cursor = { version, syncedAt: new Date().toISOString() };
182+ }
183+}
184+
185+export const connector = new OncotreeConnector();
added packages/connectors/src/connectors/oncotree/manifest.ts +34 −0
@@ -0,0 +1,34 @@
1+import { defineManifest } from '../../sdk/manifest.js';
2+
3+/**
4+ * Docs verified 2026-09-08: https://oncotree.mskcc.org (API tab) — GET /api/tumorTypes?version=…,
5+ * GET /api/versions, GET /api/mainTypes. Response fields: code, name, mainType, tissue, parent,
6+ * level, externalReferences{UMLS[],NCI[]}, history[], revocations[], precursors[], color.
7+ * License (repo README, cBioPortal/oncotree): CC BY 4.0.
8+ */
9+export const manifest = defineManifest({
10+ id: 'oncotree',
11+ name: 'OncoTree',
12+ organization: 'Memorial Sloan Kettering Cancer Center / cBioPortal',
13+ category: 'terminology',
14+ tier: 0,
15+ description: 'Open cancer classification used by cBioPortal and clinical sequencing programs. Provides a tissue-based tumor type hierarchy with NCIt and UMLS cross-references. Used as a second hierarchy dimension and to anchor cohort/genomic sources.',
16+ homepage: 'https://oncotree.mskcc.org',
17+ docsUrl: 'https://oncotree.mskcc.org/#/home?tab=api',
18+ termsUrl: 'https://github.com/cBioPortal/oncotree/blob/master/LICENSE',
19+ access: { type: 'api', auth: 'none', baseUrl: 'https://oncotree.mskcc.org/api' },
20+ license: 'CC BY 4.0',
21+ licenseStatus: 'approved',
22+ commercialUse: 'allowed',
23+ redistribution: 'attribution',
24+ attribution: 'OncoTree (Memorial Sloan Kettering Cancer Center), CC BY 4.0, https://oncotree.mskcc.org',
25+ termsReviewedAt: '2026-09-08',
26+ updateFrequency: 'Stable releases a few times per year',
27+ supportsIncrementalSync: false,
28+ entities: ['cancer', 'cancer_hierarchy', 'cancer_codes'],
29+ rateLimits: { requestsPerSecond: 2, maxConcurrency: 1 },
30+ rawRetention: 'full',
31+ documentationVerifiedAt: '2026-09-08',
32+ status: 'active',
33+ schedule: '0 4 * * 1',
34+});
added packages/connectors/src/index.ts +4 −0
@@ -0,0 +1,4 @@
1+export * from './sdk/index.js';
2+export * from './registry.js';
3+export * from './sources-sync.js';
4+export * from './planned.js';
added packages/connectors/src/planned.ts +60 −0
@@ -0,0 +1,60 @@
1+import { defineManifest, type ConnectorManifest } from './sdk/manifest.js';
2+
3+/**
4+ * Sources that are registered (visible on /sources with their compliance status) but have no
5+ * ingesting connector yet — either because license review is pending (CLAUDE.md §10.4, §142) or
6+ * because credentials are required. Nothing here ingests data.
7+ */
8+export const PLANNED_MANIFESTS: ConnectorManifest[] = [
9+ defineManifest({
10+ id: 'iarc-globocan',
11+ name: 'IARC Global Cancer Observatory (GLOBOCAN)',
12+ organization: 'International Agency for Research on Cancer (WHO)',
13+ category: 'epidemiology',
14+ tier: 0,
15+ description: 'Global estimates of cancer incidence, mortality and prevalence by country, sex, age and cancer site. The intended global epidemiology layer of CancerIndex.',
16+ homepage: 'https://gco.iarc.who.int/',
17+ docsUrl: 'https://gco.iarc.who.int/today/en/about',
18+ termsUrl: 'https://gco.iarc.who.int/en/terms-of-use',
19+ access: { type: 'api', auth: 'none', baseUrl: 'https://gco.iarc.who.int/' },
20+ license: 'IARC/WHO terms of use — redistribution rights under review',
21+ licenseStatus: 'review',
22+ commercialUse: 'unknown',
23+ redistribution: 'unknown',
24+ attribution: 'Ferlay J, Ervik M, Lam F, et al. Global Cancer Observatory: Cancer Today. Lyon, France: IARC.',
25+ termsNotes:
26+ 'Terms page is a JavaScript application; automated review on 2026-09-08 could not extract the text. Per CLAUDE.md §10.4 no automated bulk ingestion or republication of GLOBOCAN estimates until a human has reviewed the terms and, if required, obtained permission from IARC. Country/global rankings therefore remain unavailable and are shown as "awaiting license review".',
27+ updateFrequency: 'Major releases every ~2 years (GLOBOCAN 2022 current)',
28+ supportsIncrementalSync: false,
29+ entities: ['epidemiology_observations'],
30+ metrics: ['incidence_count', 'mortality_count', 'as_incidence_rate', 'as_mortality_rate', 'prevalence_5y'],
31+ rateLimits: { requestsPerSecond: 1, maxConcurrency: 1 },
32+ documentationVerifiedAt: '2026-09-08',
33+ status: 'review',
34+ }),
35+ defineManifest({
36+ id: 'seer',
37+ name: 'SEER (Surveillance, Epidemiology, and End Results)',
38+ organization: 'National Cancer Institute',
39+ category: 'epidemiology',
40+ tier: 0,
41+ description: 'US population-based registry statistics: incidence, mortality, survival, stage, age, sex, race/ethnicity. Authoritative for US relative survival.',
42+ homepage: 'https://seer.cancer.gov/',
43+ docsUrl: 'https://api.seer.cancer.gov/',
44+ termsUrl: 'https://seer.cancer.gov/about/terms.html',
45+ access: { type: 'api', auth: 'api_key', baseUrl: 'https://api.seer.cancer.gov/rest/' },
46+ license: 'US Government work (public domain); SEER*Explorer statistics citable with attribution; API requires a free key',
47+ licenseStatus: 'review',
48+ commercialUse: 'unknown',
49+ redistribution: 'attribution',
50+ attribution: 'Surveillance, Epidemiology, and End Results (SEER) Program (www.seer.cancer.gov), National Cancer Institute.',
51+ termsNotes: 'Connector is implemented against the SEER API only once a key is present (SEER_API_KEY). Status awaiting_credentials until then. Never present SEER (US) estimates as global.',
52+ updateFrequency: 'Annual (April)',
53+ supportsIncrementalSync: false,
54+ entities: ['epidemiology_observations', 'survival_observations'],
55+ metrics: ['incidence_count', 'as_incidence_rate', 'mortality_count', 'as_mortality_rate', 'five_year_survival'],
56+ rateLimits: { requestsPerSecond: 2, maxConcurrency: 1 },
57+ documentationVerifiedAt: '2026-09-08',
58+ status: 'awaiting_credentials',
59+ }),
60+];
added packages/connectors/src/registry.ts +13 −0
@@ -0,0 +1,13 @@
1+import type { Connector } from './sdk/run.js';
2+import { connector as oncotree } from './connectors/oncotree/index.js';
3+
4+/**
5+ * Connector registry. Order = recommended first-run order (CLAUDE.md §351): terminology first,
6+ * then genes, then genomics/trials/literature/variants/evidence, then epidemiology.
7+ * Add new connectors here; `pnpm cix sources:sync` seeds their manifests into `sources`.
8+ */
9+export const CONNECTORS: Connector[] = [oncotree];
10+
11+export function getConnector(id: string): Connector | undefined {
12+ return CONNECTORS.find((c) => c.manifest.id === id);
13+}
added packages/connectors/src/sdk/http.ts +162 −0
@@ -0,0 +1,162 @@
1+import { setTimeout as sleep } from 'node:timers/promises';
2+import type { ConnectorManifest } from './manifest.js';
3+
4+export interface HttpStats {
5+ requests: number;
6+ failures: number;
7+ rateLimitEvents: number;
8+ retries: number;
9+}
10+
11+export class HttpError extends Error {
12+ constructor(
13+ public readonly status: number,
14+ public readonly url: string,
15+ public readonly bodySnippet: string,
16+ ) {
17+ super(`HTTP ${status} for ${url}: ${bodySnippet.slice(0, 200)}`);
18+ }
19+}
20+
21+/** Token bucket rate limiter (CLAUDE.md §229). */
22+class TokenBucket {
23+ private tokens: number;
24+ private last = Date.now();
25+ constructor(
26+ private readonly ratePerSec: number,
27+ private readonly burst: number,
28+ ) {
29+ this.tokens = burst;
30+ }
31+ async take(): Promise<void> {
32+ for (;;) {
33+ const now = Date.now();
34+ this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) / 1000) * this.ratePerSec);
35+ this.last = now;
36+ if (this.tokens >= 1) {
37+ this.tokens -= 1;
38+ return;
39+ }
40+ await sleep(Math.ceil(((1 - this.tokens) / this.ratePerSec) * 1000));
41+ }
42+ }
43+}
44+
45+class Semaphore {
46+ private queue: Array<() => void> = [];
47+ private active = 0;
48+ constructor(private readonly max: number) {}
49+ async acquire(): Promise<() => void> {
50+ if (this.active >= this.max) await new Promise<void>((resolve) => this.queue.push(resolve));
51+ this.active++;
52+ let released = false;
53+ return () => {
54+ if (released) return;
55+ released = true;
56+ this.active--;
57+ const next = this.queue.shift();
58+ if (next) next();
59+ };
60+ }
61+}
62+
63+export interface HttpClientOptions {
64+ userAgent?: string;
65+ timeoutMs?: number;
66+ headers?: Record<string, string>;
67+}
68+
69+/**
70+ * Source-aware HTTP client: token bucket, bounded concurrency, exponential backoff with jitter,
71+ * Retry-After respect, request accounting for the ingest run.
72+ */
73+export class HttpClient {
74+ readonly stats: HttpStats = { requests: 0, failures: 0, rateLimitEvents: 0, retries: 0 };
75+ private readonly bucket: TokenBucket | null;
76+ private readonly sem: Semaphore;
77+ private readonly retry: NonNullable<ConnectorManifest['retryPolicy']>;
78+ private readonly ua: string;
79+ private readonly timeoutMs: number;
80+ private readonly headers: Record<string, string>;
81+
82+ constructor(manifest: ConnectorManifest, opts: HttpClientOptions = {}) {
83+ const rps = manifest.rateLimits.requestsPerSecond ?? (manifest.rateLimits.requestsPerMinute ? manifest.rateLimits.requestsPerMinute / 60 : undefined);
84+ this.bucket = rps ? new TokenBucket(rps, Math.max(1, Math.ceil(rps))) : null;
85+ this.sem = new Semaphore(manifest.rateLimits.maxConcurrency);
86+ this.retry = manifest.retryPolicy;
87+ // Plain product token only: some WAFs (e.g. oncotree.mskcc.org, Akamai) reject UAs containing URLs/parentheses.
88+ this.ua = opts.userAgent ?? `CancerIndex/0.1`;
89+ this.timeoutMs = opts.timeoutMs ?? 60_000;
90+ this.headers = opts.headers ?? {};
91+ }
92+
93+ /** One attempt: returns the Response (any status) or throws on network error. */
94+ private async attempt(url: string, init: RequestInit): Promise<Response> {
95+ const release = await this.sem.acquire();
96+ try {
97+ if (this.bucket) await this.bucket.take();
98+ this.stats.requests++;
99+ const controller = new AbortController();
100+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
101+ try {
102+ return await fetch(url, {
103+ ...init,
104+ headers: { 'user-agent': this.ua, accept: 'application/json, text/plain, */*', ...this.headers, ...(init.headers as Record<string, string> | undefined) },
105+ signal: controller.signal,
106+ });
107+ } finally {
108+ clearTimeout(timer);
109+ }
110+ } finally {
111+ release();
112+ }
113+ }
114+
115+ async request(url: string, init: RequestInit = {}): Promise<Response> {
116+ for (let attempt = 0; ; attempt++) {
117+ let res: Response;
118+ try {
119+ res = await this.attempt(url, init);
120+ } catch (err) {
121+ if (attempt >= this.retry.maxRetries) {
122+ this.stats.failures++;
123+ throw err;
124+ }
125+ this.stats.retries++;
126+ await sleep(this.backoff(attempt));
127+ continue;
128+ }
129+ if (res.ok) return res;
130+ const body = await res.text().catch(() => '');
131+ if (res.status === 429) this.stats.rateLimitEvents++;
132+ const retryable = res.status === 429 || res.status >= 500 || res.status === 408;
133+ if (!retryable || attempt >= this.retry.maxRetries) {
134+ this.stats.failures++;
135+ throw new HttpError(res.status, url, body);
136+ }
137+ const retryAfter = Number(res.headers.get('retry-after'));
138+ const delay = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1000, 120_000) : this.backoff(attempt);
139+ this.stats.retries++;
140+ await sleep(delay);
141+ }
142+ }
143+
144+ async json<T = unknown>(url: string, init?: RequestInit): Promise<T> {
145+ const res = await this.request(url, init);
146+ return (await res.json()) as T;
147+ }
148+
149+ async text(url: string, init?: RequestInit): Promise<string> {
150+ const res = await this.request(url, init);
151+ return await res.text();
152+ }
153+
154+ async postJson<T = unknown>(url: string, body: unknown, init: RequestInit = {}): Promise<T> {
155+ return this.json<T>(url, { ...init, method: 'POST', headers: { 'content-type': 'application/json', ...(init.headers as Record<string, string> | undefined) }, body: JSON.stringify(body) });
156+ }
157+
158+ private backoff(attempt: number): number {
159+ const base = Math.min(this.retry.maxDelayMs, this.retry.baseDelayMs * 2 ** attempt);
160+ return Math.round(base / 2 + Math.random() * (base / 2));
161+ }
162+}
added packages/connectors/src/sdk/index.ts +5 −0
@@ -0,0 +1,5 @@
1+export * from './manifest.js';
2+export * from './http.js';
3+export * from './lake.js';
4+export * from './run.js';
5+export * from './validate.js';
added packages/connectors/src/sdk/lake.ts +105 −0
@@ -0,0 +1,105 @@
1+import { createWriteStream, existsSync, mkdirSync, createReadStream } from 'node:fs';
2+import { mkdir, stat } from 'node:fs/promises';
3+import path from 'node:path';
4+import { createGzip, createGunzip } from 'node:zlib';
5+import { pipeline } from 'node:stream/promises';
6+import { createInterface } from 'node:readline';
7+import { dataDir } from '@cancerindex/shared';
8+
9+/**
10+ * Raw data lake (CLAUDE.md §26): every source payload retained when licensing allows.
11+ * Layout: {CI_DATA_DIR}/raw/{source}/{YYYY-MM-DD}/{entity}/{runId}-{part}.jsonl.gz
12+ * A record's `rawPath` is "<file>#<line>" so any canonical value can be traced to its raw payload.
13+ */
14+export class RawLake {
15+ private readonly root: string;
16+ private writers = new Map<string, { path: string; stream: ReturnType<typeof createWriteStream>; gz: ReturnType<typeof createGzip>; lines: number; part: number; bytes: number }>();
17+ constructor(
18+ private readonly source: string,
19+ private readonly runId: string,
20+ private readonly date = new Date(),
21+ root?: string,
22+ ) {
23+ this.root = root ?? path.join(dataDir(), 'raw');
24+ }
25+
26+ dir(entity: string): string {
27+ const day = this.date.toISOString().slice(0, 10);
28+ return path.join(this.root, this.source, day, entity);
29+ }
30+
31+ /** Append one raw JSON payload; returns its rawPath reference. */
32+ async put(entity: string, payload: unknown): Promise<string> {
33+ let w = this.writers.get(entity);
34+ if (!w || w.bytes > 64 * 1024 * 1024) {
35+ if (w) await this.closeWriter(entity);
36+ const part = (w?.part ?? 0) + 1;
37+ const dir = this.dir(entity);
38+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
39+ const file = path.join(dir, `${this.runId}-${String(part).padStart(3, '0')}.jsonl.gz`);
40+ const stream = createWriteStream(file);
41+ const gz = createGzip({ level: 6 });
42+ gz.pipe(stream);
43+ w = { path: file, stream, gz, lines: 0, part, bytes: 0 };
44+ this.writers.set(entity, w);
45+ }
46+ const line = JSON.stringify(payload) + '\n';
47+ w.bytes += line.length;
48+ const ref = `${w.path}#${w.lines}`;
49+ w.lines++;
50+ if (!w.gz.write(line)) await new Promise<void>((r) => w!.gz.once('drain', () => r()));
51+ return ref;
52+ }
53+
54+ private async closeWriter(entity: string): Promise<void> {
55+ const w = this.writers.get(entity);
56+ if (!w) return;
57+ await new Promise<void>((resolve, reject) => {
58+ w.stream.once('finish', () => resolve());
59+ w.stream.once('error', reject);
60+ w.gz.end();
61+ });
62+ this.writers.delete(entity);
63+ }
64+
65+ async close(): Promise<void> {
66+ for (const entity of [...this.writers.keys()]) await this.closeWriter(entity);
67+ }
68+
69+ /** Read a raw payload back by reference (admin TRACE, reprocessing). */
70+ static async read(ref: string): Promise<unknown | null> {
71+ const [file, lineStr] = ref.split('#');
72+ if (!file || lineStr === undefined) return null;
73+ const target = Number(lineStr);
74+ const rl = createInterface({ input: createReadStream(file).pipe(createGunzip()) });
75+ let i = 0;
76+ for await (const line of rl) {
77+ if (i === target) {
78+ rl.close();
79+ return JSON.parse(line);
80+ }
81+ i++;
82+ }
83+ return null;
84+ }
85+}
86+
87+/** Download a bulk file to the lake (CLAUDE.md §228-230) with checksum + size recorded. */
88+export async function downloadBulk(url: string, dest: string, fetchImpl: typeof fetch = fetch, headers: Record<string, string> = {}): Promise<{ path: string; bytes: number; sha256: string }> {
89+ await mkdir(path.dirname(dest), { recursive: true });
90+ const res = await fetchImpl(url, { headers: { 'user-agent': 'CancerIndex/0.1 (+https://www.cancerindex.io)', ...headers } });
91+ if (!res.ok || !res.body) throw new Error(`bulk download failed ${res.status} ${url}`);
92+ const { createHash } = await import('node:crypto');
93+ const hash = createHash('sha256');
94+ const { Transform } = await import('node:stream');
95+ const tap = new Transform({
96+ transform(chunk, _enc, cb) {
97+ hash.update(chunk);
98+ cb(null, chunk);
99+ },
100+ });
101+ const { Readable } = await import('node:stream');
102+ await pipeline(Readable.fromWeb(res.body as never), tap, createWriteStream(dest));
103+ const s = await stat(dest);
104+ return { path: dest, bytes: s.size, sha256: hash.digest('hex') };
105+}
added packages/connectors/src/sdk/manifest.ts +61 −0
@@ -0,0 +1,61 @@
1+import { z } from 'zod';
2+
3+/** Connector manifest (CLAUDE.md §9). Seeds the `sources` table; drives the public /source pages. */
4+export const ConnectorManifest = z.object({
5+ id: z.string().regex(/^[a-z0-9-]+$/),
6+ name: z.string(),
7+ organization: z.string(),
8+ category: z.enum(['terminology', 'genes', 'genomics', 'variants', 'epidemiology', 'trials', 'literature', 'drugs', 'regulatory', 'evidence', 'pathways', 'funding', 'guidelines']),
9+ tier: z.number().int().min(0).max(11),
10+ description: z.string(),
11+ homepage: z.string().url(),
12+ docsUrl: z.string().url().optional(),
13+ termsUrl: z.string().url().optional(),
14+ access: z.object({
15+ type: z.enum(['api', 'bulk', 'rss', 'ftp', 'graphql', 'rest', 'scrape', 'manual']),
16+ auth: z.enum(['none', 'api_key', 'oauth', 'account', 'controlled']),
17+ baseUrl: z.string().url().optional(),
18+ }),
19+ license: z.string(),
20+ licenseStatus: z.enum(['review', 'approved', 'restricted', 'blocked']),
21+ commercialUse: z.enum(['allowed', 'restricted', 'prohibited', 'unknown']),
22+ redistribution: z.enum(['allowed', 'attribution', 'restricted', 'prohibited', 'unknown']),
23+ attribution: z.string().optional(),
24+ termsReviewedAt: z.string().optional(), // YYYY-MM-DD, human review date
25+ termsNotes: z.string().optional(),
26+ updateFrequency: z.string(),
27+ expectedLatency: z.string().optional(),
28+ supportsIncrementalSync: z.boolean(),
29+ entities: z.array(z.string()),
30+ metrics: z.array(z.string()).default([]),
31+ rateLimits: z.object({
32+ requestsPerSecond: z.number().positive().optional(),
33+ requestsPerMinute: z.number().positive().optional(),
34+ maxConcurrency: z.number().int().positive().default(1),
35+ notes: z.string().optional(),
36+ }),
37+ retryPolicy: z
38+ .object({
39+ maxRetries: z.number().int().min(0).default(4),
40+ baseDelayMs: z.number().int().min(0).default(500),
41+ maxDelayMs: z.number().int().min(0).default(30_000),
42+ })
43+ .default({ maxRetries: 4, baseDelayMs: 500, maxDelayMs: 30_000 }),
44+ rawRetention: z.enum(['full', 'summary', 'none']).default('full'),
45+ schemaVersion: z.string().default('1'),
46+ documentationVerifiedAt: z.string(), // YYYY-MM-DD, the date official docs were checked (CLAUDE.md §224-225)
47+ owner: z.string().default('cancerindex'),
48+ status: z.enum(['planned', 'active', 'paused', 'awaiting_credentials', 'review', 'retired']),
49+ schedule: z.string().optional(), // cron expression for the worker scheduler
50+ anomalyGuard: z
51+ .object({
52+ /** Refuse to mark records source_missing when the fetched count drops below this ratio of the previous run. */
53+ minRatioOfPrevious: z.number().min(0).max(1).default(0.5),
54+ })
55+ .default({ minRatioOfPrevious: 0.5 }),
56+});
57+export type ConnectorManifest = z.infer<typeof ConnectorManifest>;
58+
59+export function defineManifest(m: z.input<typeof ConnectorManifest>): ConnectorManifest {
60+ return ConnectorManifest.parse(m);
61+}
added packages/connectors/src/sdk/run.ts +0 −0

Binary file not shown.

added packages/connectors/src/sdk/validate.ts +49 −0
@@ -0,0 +1,49 @@
1+/** Scientific invariants applied to normalized records (CLAUDE.md §110, §154). */
2+
3+export interface ValidationIssue {
4+ field: string;
5+ message: string;
6+}
7+
8+export function validateEpidemiology(o: { metric: string; value: number; year: number; lowerCi?: number | null; upperCi?: number | null; unit: string }): ValidationIssue[] {
9+ const issues: ValidationIssue[] = [];
10+ if (!Number.isFinite(o.value)) issues.push({ field: 'value', message: 'not finite' });
11+ if (o.value < 0) issues.push({ field: 'value', message: 'negative count/rate' });
12+ if (o.year < 1900 || o.year > new Date().getFullYear() + 30) issues.push({ field: 'year', message: `implausible year ${o.year}` });
13+ if (o.lowerCi != null && o.lowerCi > o.value) issues.push({ field: 'lowerCi', message: 'lowerCI > estimate' });
14+ if (o.upperCi != null && o.upperCi < o.value) issues.push({ field: 'upperCi', message: 'upperCI < estimate' });
15+ if (o.metric.endsWith('_count') && o.unit !== 'count') issues.push({ field: 'unit', message: 'count metric must use unit=count' });
16+ if (o.metric.endsWith('_rate') && o.unit !== 'per_100k') issues.push({ field: 'unit', message: 'rate metric must use unit=per_100k' });
17+ return issues;
18+}
19+
20+export function validateSurvival(o: { probability?: number | null; durationMonths: number; lowerCi?: number | null; upperCi?: number | null; cohortSize?: number | null }): ValidationIssue[] {
21+ const issues: ValidationIssue[] = [];
22+ if (o.probability != null && (o.probability < 0 || o.probability > 1)) issues.push({ field: 'probability', message: 'survival must be within [0,1]' });
23+ if (o.durationMonths <= 0) issues.push({ field: 'durationMonths', message: 'duration must be positive' });
24+ if (o.probability != null && o.lowerCi != null && o.lowerCi > o.probability) issues.push({ field: 'lowerCi', message: 'lowerCI > estimate' });
25+ if (o.probability != null && o.upperCi != null && o.upperCi < o.probability) issues.push({ field: 'upperCi', message: 'upperCI < estimate' });
26+ if (o.cohortSize != null && o.cohortSize < 0) issues.push({ field: 'cohortSize', message: 'negative cohort' });
27+ return issues;
28+}
29+
30+export const TRIAL_PHASES = new Set(['EARLY_PHASE1', 'PHASE1', 'PHASE2', 'PHASE3', 'PHASE4', 'NA']);
31+export const TRIAL_STATUSES = new Set(['ACTIVE_NOT_RECRUITING', 'COMPLETED', 'ENROLLING_BY_INVITATION', 'NOT_YET_RECRUITING', 'RECRUITING', 'SUSPENDED', 'TERMINATED', 'WITHDRAWN', 'AVAILABLE', 'NO_LONGER_AVAILABLE', 'TEMPORARILY_NOT_AVAILABLE', 'APPROVED_FOR_MARKETING', 'WITHHELD', 'UNKNOWN']);
32+
33+export function validateTrial(o: { nctId: string; phases: string[]; overallStatus?: string | null }): ValidationIssue[] {
34+ const issues: ValidationIssue[] = [];
35+ if (!/^NCT\d{8}$/.test(o.nctId)) issues.push({ field: 'nctId', message: 'malformed NCT id' });
36+ for (const p of o.phases) if (!TRIAL_PHASES.has(p)) issues.push({ field: 'phases', message: `unknown phase enum ${p}` });
37+ if (o.overallStatus && !TRIAL_STATUSES.has(o.overallStatus)) issues.push({ field: 'overallStatus', message: `unknown status enum ${o.overallStatus}` });
38+ return issues;
39+}
40+
41+export function validateFrequency(o: { casesAffected: number; casesProfiled: number }): ValidationIssue[] {
42+ const issues: ValidationIssue[] = [];
43+ if (o.casesProfiled <= 0) issues.push({ field: 'casesProfiled', message: 'denominator must be positive' });
44+ if (o.casesAffected < 0 || o.casesAffected > o.casesProfiled) issues.push({ field: 'casesAffected', message: 'numerator out of range' });
45+ return issues;
46+}
47+
48+/** Gene symbol sanity (HGNC style). */
49+export const GENE_SYMBOL_RE = /^[A-Z][A-Z0-9-]{0,15}(?:@|orf\d+)?$/i;
added packages/connectors/src/sources-sync.ts +51 −0
@@ -0,0 +1,51 @@
1+import { eq } from 'drizzle-orm';
2+import { type Database, sources, mintId } from '@cancerindex/database';
3+import { CONNECTORS } from './registry.js';
4+import type { ConnectorManifest } from './sdk/manifest.js';
5+
6+/** Sync connector manifests into the `sources` registry (CLAUDE.md §95, §142, §307). Idempotent. */
7+export async function syncSources(db: Database, extra: ConnectorManifest[] = []): Promise<{ created: number; updated: number }> {
8+ let created = 0;
9+ let updated = 0;
10+ const manifests = [...CONNECTORS.map((c) => c.manifest), ...extra];
11+ for (const m of manifests) {
12+ const values = {
13+ slug: m.id,
14+ name: m.name,
15+ organization: m.organization,
16+ category: m.category,
17+ description: m.description,
18+ homepage: m.homepage,
19+ docsUrl: m.docsUrl ?? null,
20+ termsUrl: m.termsUrl ?? null,
21+ accessType: m.access.type,
22+ accessAuth: m.access.auth,
23+ license: m.license,
24+ licenseStatus: m.licenseStatus,
25+ commercialUse: m.commercialUse,
26+ redistribution: m.redistribution,
27+ attribution: m.attribution ?? null,
28+ licenseReviewedAt: m.termsReviewedAt ? new Date(m.termsReviewedAt) : null,
29+ approvedForProduction: m.licenseStatus === 'approved',
30+ updateFrequency: m.updateFrequency,
31+ supportsIncremental: m.supportsIncrementalSync,
32+ entities: m.entities,
33+ metrics: m.metrics,
34+ rateLimit: m.rateLimits.requestsPerSecond ? `${m.rateLimits.requestsPerSecond} req/s` : m.rateLimits.requestsPerMinute ? `${m.rateLimits.requestsPerMinute} req/min` : null,
35+ tier: m.tier,
36+ manifest: m as unknown as Record<string, unknown>,
37+ updatedAt: new Date(),
38+ };
39+ const [existing] = await db.select({ id: sources.id, status: sources.status }).from(sources).where(eq(sources.slug, m.id)).limit(1);
40+ if (existing) {
41+ // Keep runtime status (active/degraded) unless manifest declares a gating status.
42+ const gating = ['awaiting_credentials', 'review', 'retired', 'paused'].includes(m.status);
43+ await db.update(sources).set({ ...values, ...(gating ? { status: m.status } : {}) }).where(eq(sources.id, existing.id));
44+ updated++;
45+ } else {
46+ await db.insert(sources).values({ id: await mintId(db, 'SOURCE'), ...values, status: m.status });
47+ created++;
48+ }
49+ }
50+ return { created, updated };
51+}
added packages/connectors/tsconfig.json +8 −0
@@ -0,0 +1,8 @@
1+{
2+ "extends": "../../tsconfig.base.json",
3+ "compilerOptions": {
4+ "rootDir": ".",
5+ "noEmit": true
6+ },
7+ "include": ["src", "test"]
8+}
added packages/connectors/vitest.config.ts +8 −0
@@ -0,0 +1,8 @@
1+import { defineConfig } from 'vitest/config';
2+
3+export default defineConfig({
4+ test: {
5+ include: ['src/**/*.test.ts', 'test/**/*.test.ts'],
6+ testTimeout: 20_000,
7+ },
8+});
added packages/database/drizzle.config.ts +11 −0
@@ -0,0 +1,11 @@
1+import { defineConfig } from 'drizzle-kit';
2+
3+export default defineConfig({
4+ dialect: 'postgresql',
5+ schema: './src/schema/index.ts',
6+ out: './migrations',
7+ dbCredentials: { url: process.env.DATABASE_URL ?? 'postgres://localhost:5432/cancerindex' },
8+ casing: 'snake_case',
9+ strict: true,
10+ verbose: true,
11+});
added packages/database/migrations/0000_small_blazing_skull.sql +936 −0
@@ -0,0 +1,936 @@
1+CREATE TABLE "audit_log" (
2+ "id" bigserial PRIMARY KEY NOT NULL,
3+ "actor" text NOT NULL,
4+ "action" text NOT NULL,
5+ "entity_type" text,
6+ "entity_id" text,
7+ "before" jsonb,
8+ "after" jsonb,
9+ "reason" text,
10+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
11+);
12+--> statement-breakpoint
13+CREATE TABLE "change_events" (
14+ "id" bigserial PRIMARY KEY NOT NULL,
15+ "entity_type" text NOT NULL,
16+ "entity_id" text NOT NULL,
17+ "kind" text NOT NULL,
18+ "summary" text NOT NULL,
19+ "before" jsonb,
20+ "after" jsonb,
21+ "ingest_run_id" text,
22+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
23+);
24+--> statement-breakpoint
25+CREATE TABLE "connector_cursors" (
26+ "connector_id" text PRIMARY KEY NOT NULL,
27+ "cursor" jsonb DEFAULT '{}'::jsonb NOT NULL,
28+ "last_success_at" timestamp with time zone,
29+ "last_attempt_at" timestamp with time zone,
30+ "paused" boolean DEFAULT false NOT NULL,
31+ "health" text DEFAULT 'unknown' NOT NULL,
32+ "health_detail" text,
33+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
34+);
35+--> statement-breakpoint
36+CREATE TABLE "connector_field_stats" (
37+ "id" bigserial PRIMARY KEY NOT NULL,
38+ "connector_id" text NOT NULL,
39+ "entity" text NOT NULL,
40+ "field" text NOT NULL,
41+ "types" text[] DEFAULT '{}' NOT NULL,
42+ "seen_count" integer DEFAULT 0 NOT NULL,
43+ "null_count" integer DEFAULT 0 NOT NULL,
44+ "first_seen_run" text,
45+ "last_seen_run" text,
46+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
47+);
48+--> statement-breakpoint
49+CREATE TABLE "entity_merges" (
50+ "id" bigserial PRIMARY KEY NOT NULL,
51+ "entity_type" text NOT NULL,
52+ "keep_id" text NOT NULL,
53+ "merge_id" text NOT NULL,
54+ "evidence" jsonb DEFAULT '{}'::jsonb NOT NULL,
55+ "status" text DEFAULT 'proposed' NOT NULL,
56+ "decided_by" text,
57+ "decided_at" timestamp with time zone,
58+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
59+);
60+--> statement-breakpoint
61+CREATE TABLE "id_sequences" (
62+ "namespace" varchar(16) PRIMARY KEY NOT NULL,
63+ "next" bigint DEFAULT 1 NOT NULL
64+);
65+--> statement-breakpoint
66+CREATE TABLE "ingest_runs" (
67+ "id" text PRIMARY KEY NOT NULL,
68+ "connector_id" text NOT NULL,
69+ "source_id" varchar(32) NOT NULL,
70+ "mode" text DEFAULT 'incremental' NOT NULL,
71+ "status" text DEFAULT 'running' NOT NULL,
72+ "started_at" timestamp with time zone DEFAULT now() NOT NULL,
73+ "finished_at" timestamp with time zone,
74+ "duration_ms" integer,
75+ "records_fetched" integer DEFAULT 0 NOT NULL,
76+ "records_created" integer DEFAULT 0 NOT NULL,
77+ "records_updated" integer DEFAULT 0 NOT NULL,
78+ "records_unchanged" integer DEFAULT 0 NOT NULL,
79+ "records_rejected" integer DEFAULT 0 NOT NULL,
80+ "http_requests" integer DEFAULT 0 NOT NULL,
81+ "http_failures" integer DEFAULT 0 NOT NULL,
82+ "rate_limit_events" integer DEFAULT 0 NOT NULL,
83+ "validation_failures" integer DEFAULT 0 NOT NULL,
84+ "schema_drift" jsonb DEFAULT '[]'::jsonb NOT NULL,
85+ "cursor_before" jsonb,
86+ "cursor_after" jsonb,
87+ "error" text,
88+ "log" jsonb DEFAULT '[]'::jsonb NOT NULL,
89+ "dataset_version" text,
90+ "anomaly" text
91+);
92+--> statement-breakpoint
93+CREATE TABLE "provenance" (
94+ "id" bigserial PRIMARY KEY NOT NULL,
95+ "public_id" varchar(32),
96+ "source_id" varchar(32) NOT NULL,
97+ "source_record_id" text,
98+ "source_url" text,
99+ "dataset" text,
100+ "dataset_version" text,
101+ "publication_id" varchar(32),
102+ "pmid" text,
103+ "doi" text,
104+ "retrieved_at" timestamp with time zone NOT NULL,
105+ "published_at" text,
106+ "updated_at_source" text,
107+ "geography" text,
108+ "population" text,
109+ "cohort_size" integer,
110+ "methodology" text,
111+ "evidence_type" text NOT NULL,
112+ "access_level" text DEFAULT 'open' NOT NULL,
113+ "confidence" real,
114+ "license" text,
115+ "ingest_run_id" text,
116+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
117+);
118+--> statement-breakpoint
119+CREATE TABLE "source_records" (
120+ "id" bigserial PRIMARY KEY NOT NULL,
121+ "source_id" varchar(32) NOT NULL,
122+ "entity_kind" text NOT NULL,
123+ "source_record_id" text NOT NULL,
124+ "payload_hash" text NOT NULL,
125+ "raw_path" text,
126+ "status" text DEFAULT 'active' NOT NULL,
127+ "first_seen_run" text,
128+ "last_seen_run" text,
129+ "retrieved_at" timestamp with time zone DEFAULT now() NOT NULL,
130+ "source_updated_at" timestamp with time zone,
131+ "canonical_type" text,
132+ "canonical_id" text,
133+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
134+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
135+);
136+--> statement-breakpoint
137+CREATE TABLE "sources" (
138+ "id" varchar(32) PRIMARY KEY NOT NULL,
139+ "slug" text NOT NULL,
140+ "name" text NOT NULL,
141+ "organization" text,
142+ "category" text NOT NULL,
143+ "description" text,
144+ "homepage" text,
145+ "docs_url" text,
146+ "terms_url" text,
147+ "access_type" text NOT NULL,
148+ "access_auth" text NOT NULL,
149+ "license" text,
150+ "license_status" text DEFAULT 'review' NOT NULL,
151+ "commercial_use" text DEFAULT 'unknown' NOT NULL,
152+ "redistribution" text DEFAULT 'unknown' NOT NULL,
153+ "attribution" text,
154+ "license_reviewed_at" timestamp with time zone,
155+ "approved_for_production" boolean DEFAULT false NOT NULL,
156+ "update_frequency" text,
157+ "supports_incremental" boolean DEFAULT false NOT NULL,
158+ "entities" text[] DEFAULT '{}' NOT NULL,
159+ "metrics" text[] DEFAULT '{}' NOT NULL,
160+ "rate_limit" text,
161+ "status" text DEFAULT 'planned' NOT NULL,
162+ "tier" integer DEFAULT 0 NOT NULL,
163+ "manifest" jsonb DEFAULT '{}'::jsonb NOT NULL,
164+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
165+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
166+);
167+--> statement-breakpoint
168+CREATE TABLE "unresolved_labels" (
169+ "id" bigserial PRIMARY KEY NOT NULL,
170+ "source_id" varchar(32) NOT NULL,
171+ "entity_kind" text NOT NULL,
172+ "source_text" text NOT NULL,
173+ "normalized" text NOT NULL,
174+ "context" jsonb DEFAULT '{}'::jsonb NOT NULL,
175+ "count" integer DEFAULT 1 NOT NULL,
176+ "status" text DEFAULT 'open' NOT NULL,
177+ "suggested_id" text,
178+ "suggested_match_type" text,
179+ "suggested_score" real,
180+ "resolved_id" text,
181+ "resolved_by" text,
182+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
183+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
184+);
185+--> statement-breakpoint
186+CREATE TABLE "anatomical_sites" (
187+ "id" varchar(32) PRIMARY KEY NOT NULL,
188+ "name" text NOT NULL,
189+ "slug" text NOT NULL,
190+ "ncit_code" text,
191+ "uberon_id" text,
192+ "parent_id" varchar(32),
193+ "system" text
194+);
195+--> statement-breakpoint
196+CREATE TABLE "cancer_aliases" (
197+ "id" bigserial PRIMARY KEY NOT NULL,
198+ "cancer_id" varchar(32) NOT NULL,
199+ "alias" text NOT NULL,
200+ "normalized" text NOT NULL,
201+ "alias_type" text DEFAULT 'synonym' NOT NULL,
202+ "source_id" varchar(32),
203+ "source_terminology" text,
204+ "language" text DEFAULT 'en' NOT NULL
205+);
206+--> statement-breakpoint
207+CREATE TABLE "cancer_anatomy" (
208+ "id" bigserial PRIMARY KEY NOT NULL,
209+ "cancer_id" varchar(32) NOT NULL,
210+ "site_id" varchar(32) NOT NULL,
211+ "relation" text DEFAULT 'primary' NOT NULL,
212+ "source_id" varchar(32)
213+);
214+--> statement-breakpoint
215+CREATE TABLE "cancer_codes" (
216+ "id" bigserial PRIMARY KEY NOT NULL,
217+ "cancer_id" varchar(32) NOT NULL,
218+ "system" text NOT NULL,
219+ "code" text NOT NULL,
220+ "match_type" text DEFAULT 'EXACT_IDENTIFIER' NOT NULL,
221+ "source_id" varchar(32),
222+ "valid_from" text,
223+ "valid_to" text
224+);
225+--> statement-breakpoint
226+CREATE TABLE "cancer_hierarchy" (
227+ "id" bigserial PRIMARY KEY NOT NULL,
228+ "parent_id" varchar(32) NOT NULL,
229+ "child_id" varchar(32) NOT NULL,
230+ "hierarchy_type" text NOT NULL,
231+ "source_id" varchar(32)
232+);
233+--> statement-breakpoint
234+CREATE TABLE "cancers" (
235+ "id" varchar(32) PRIMARY KEY NOT NULL,
236+ "slug" text NOT NULL,
237+ "canonical_name" text NOT NULL,
238+ "short_name" text,
239+ "entity_type" text DEFAULT 'cancer' NOT NULL,
240+ "malignant" boolean DEFAULT true NOT NULL,
241+ "solid_tumor" boolean DEFAULT true NOT NULL,
242+ "hematologic" boolean DEFAULT false NOT NULL,
243+ "pediatric_relevant" boolean DEFAULT false NOT NULL,
244+ "rare_cancer" boolean,
245+ "top_level" boolean DEFAULT false NOT NULL,
246+ "description" text,
247+ "description_provenance_id" integer,
248+ "primary_ncit_code" text,
249+ "primary_oncotree_code" text,
250+ "depth" integer DEFAULT 0 NOT NULL,
251+ "status" text DEFAULT 'active' NOT NULL,
252+ "merged_into" varchar(32),
253+ "deprecated_reason" text,
254+ "classification_version" text,
255+ "semantic_types" text[] DEFAULT '{}' NOT NULL,
256+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
257+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
258+);
259+--> statement-breakpoint
260+CREATE TABLE "cohort_definitions" (
261+ "id" bigserial PRIMARY KEY NOT NULL,
262+ "name" text NOT NULL,
263+ "cancer_id" varchar(32) NOT NULL,
264+ "biomarker_ids" text[] DEFAULT '{}' NOT NULL,
265+ "variant_ids" text[] DEFAULT '{}' NOT NULL,
266+ "stage" text,
267+ "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
268+ "confidence" real,
269+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
270+);
271+--> statement-breakpoint
272+CREATE TABLE "geographies" (
273+ "id" varchar(32) PRIMARY KEY NOT NULL,
274+ "slug" text NOT NULL,
275+ "name" text NOT NULL,
276+ "kind" text NOT NULL,
277+ "iso2" text,
278+ "iso3" text,
279+ "parent_id" varchar(32),
280+ "who_region" text,
281+ "population" integer,
282+ "population_year" integer
283+);
284+--> statement-breakpoint
285+CREATE TABLE "biomarkers" (
286+ "id" varchar(32) PRIMARY KEY NOT NULL,
287+ "slug" text NOT NULL,
288+ "name" text NOT NULL,
289+ "kind" text NOT NULL,
290+ "gene_id" varchar(32),
291+ "ncit_code" text,
292+ "description" text,
293+ "measurement" jsonb DEFAULT '{}'::jsonb NOT NULL,
294+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
295+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
296+);
297+--> statement-breakpoint
298+CREATE TABLE "cancer_gene_frequencies" (
299+ "id" bigserial PRIMARY KEY NOT NULL,
300+ "cohort_id" varchar(32) NOT NULL,
301+ "cancer_id" varchar(32),
302+ "gene_id" varchar(32),
303+ "gene_symbol" text NOT NULL,
304+ "alteration_type" text DEFAULT 'ssm' NOT NULL,
305+ "cases_affected" integer NOT NULL,
306+ "cases_profiled" integer NOT NULL,
307+ "frequency" real NOT NULL,
308+ "rank" integer,
309+ "data_release" text,
310+ "provenance_id" integer NOT NULL,
311+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
312+);
313+--> statement-breakpoint
314+CREATE TABLE "entity_embeddings" (
315+ "id" bigserial PRIMARY KEY NOT NULL,
316+ "entity_type" text NOT NULL,
317+ "entity_id" text NOT NULL,
318+ "model" text NOT NULL,
319+ "dimensions" integer NOT NULL,
320+ "text_hash" text NOT NULL,
321+ "embedding" text,
322+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
323+);
324+--> statement-breakpoint
325+CREATE TABLE "gene_aliases" (
326+ "id" bigserial PRIMARY KEY NOT NULL,
327+ "gene_id" varchar(32) NOT NULL,
328+ "alias" text NOT NULL,
329+ "alias_type" text NOT NULL,
330+ "source_id" varchar(32)
331+);
332+--> statement-breakpoint
333+CREATE TABLE "genes" (
334+ "id" varchar(32) PRIMARY KEY NOT NULL,
335+ "hgnc_id" text,
336+ "symbol" text NOT NULL,
337+ "name" text,
338+ "locus_type" text,
339+ "locus_group" text,
340+ "location" text,
341+ "chromosome" text,
342+ "ensembl_gene_id" text,
343+ "ncbi_gene_id" text,
344+ "omim_ids" text[] DEFAULT '{}' NOT NULL,
345+ "uniprot_ids" text[] DEFAULT '{}' NOT NULL,
346+ "refseq_accession" text,
347+ "prev_symbols" text[] DEFAULT '{}' NOT NULL,
348+ "alias_symbols" text[] DEFAULT '{}' NOT NULL,
349+ "gene_families" text[] DEFAULT '{}' NOT NULL,
350+ "status" text DEFAULT 'Approved' NOT NULL,
351+ "is_cancer_gene" boolean DEFAULT false NOT NULL,
352+ "civic_gene_id" integer,
353+ "description" text,
354+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
355+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
356+);
357+--> statement-breakpoint
358+CREATE TABLE "genomic_cohorts" (
359+ "id" varchar(32) PRIMARY KEY NOT NULL,
360+ "source_id" varchar(32) NOT NULL,
361+ "study_id" text NOT NULL,
362+ "name" text NOT NULL,
363+ "program" text,
364+ "primary_sites" text[] DEFAULT '{}' NOT NULL,
365+ "disease_types" text[] DEFAULT '{}' NOT NULL,
366+ "cancer_id" varchar(32),
367+ "cancer_match_type" text,
368+ "case_count" integer,
369+ "cases_with_ssm" integer,
370+ "data_release" text,
371+ "access_level" text DEFAULT 'open' NOT NULL,
372+ "url" text,
373+ "provenance_id" integer,
374+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
375+);
376+--> statement-breakpoint
377+CREATE TABLE "variant_aliases" (
378+ "id" bigserial PRIMARY KEY NOT NULL,
379+ "variant_id" varchar(32) NOT NULL,
380+ "alias" text NOT NULL,
381+ "source_id" varchar(32)
382+);
383+--> statement-breakpoint
384+CREATE TABLE "variant_clinical_significance" (
385+ "id" bigserial PRIMARY KEY NOT NULL,
386+ "variant_id" varchar(32) NOT NULL,
387+ "clinvar_variation_id" text NOT NULL,
388+ "clinical_significance" text NOT NULL,
389+ "review_status" text,
390+ "star_rating" integer,
391+ "last_evaluated" text,
392+ "conditions" text[] DEFAULT '{}' NOT NULL,
393+ "condition_cancer_ids" text[] DEFAULT '{}' NOT NULL,
394+ "origin_simple" text,
395+ "number_submitters" integer,
396+ "provenance_id" integer NOT NULL,
397+ "ingest_run_id" text,
398+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
399+);
400+--> statement-breakpoint
401+CREATE TABLE "variants" (
402+ "id" varchar(32) PRIMARY KEY NOT NULL,
403+ "slug" text NOT NULL,
404+ "gene_id" varchar(32),
405+ "gene_symbol" text,
406+ "name" text NOT NULL,
407+ "variant_type" text,
408+ "hgvs_g" text,
409+ "hgvs_c" text,
410+ "hgvs_p" text,
411+ "assembly" text,
412+ "chromosome" text,
413+ "start" integer,
414+ "end" integer,
415+ "reference_bases" text,
416+ "alternate_bases" text,
417+ "coordinates" jsonb DEFAULT '[]'::jsonb NOT NULL,
418+ "clinvar_variation_id" text,
419+ "civic_variant_id" integer,
420+ "dbsnp_ids" text[] DEFAULT '{}' NOT NULL,
421+ "fusion_partners" text[] DEFAULT '{}' NOT NULL,
422+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
423+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
424+);
425+--> statement-breakpoint
426+CREATE TABLE "drug_aliases" (
427+ "id" bigserial PRIMARY KEY NOT NULL,
428+ "drug_id" varchar(32) NOT NULL,
429+ "alias" text NOT NULL,
430+ "normalized" text NOT NULL,
431+ "alias_type" text DEFAULT 'synonym' NOT NULL,
432+ "source_id" varchar(32)
433+);
434+--> statement-breakpoint
435+CREATE TABLE "drug_approvals" (
436+ "id" bigserial PRIMARY KEY NOT NULL,
437+ "drug_id" varchar(32) NOT NULL,
438+ "cancer_id" varchar(32),
439+ "biomarker_ids" text[] DEFAULT '{}' NOT NULL,
440+ "tumor_agnostic" boolean DEFAULT false NOT NULL,
441+ "jurisdiction" text NOT NULL,
442+ "authority" text NOT NULL,
443+ "indication" text NOT NULL,
444+ "line_of_therapy" text,
445+ "disease_stage" text,
446+ "approval_type" text,
447+ "accelerated" boolean,
448+ "conditional" boolean,
449+ "approval_date" text,
450+ "withdrawal_date" text,
451+ "status" text NOT NULL,
452+ "application_number" text,
453+ "source_id" varchar(32) NOT NULL,
454+ "provenance_id" integer NOT NULL,
455+ "raw" jsonb,
456+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
457+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
458+);
459+--> statement-breakpoint
460+CREATE TABLE "drugs" (
461+ "id" varchar(32) PRIMARY KEY NOT NULL,
462+ "slug" text NOT NULL,
463+ "name" text NOT NULL,
464+ "kind" text,
465+ "ncit_code" text,
466+ "chembl_id" text,
467+ "civic_therapy_id" integer,
468+ "drugbank_id" text,
469+ "pubchem_cid" text,
470+ "unii" text,
471+ "mechanism" text,
472+ "target_gene_ids" text[] DEFAULT '{}' NOT NULL,
473+ "development_status" text,
474+ "description" text,
475+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
476+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
477+);
478+--> statement-breakpoint
479+CREATE TABLE "treatment_regimens" (
480+ "id" varchar(32) PRIMARY KEY NOT NULL,
481+ "slug" text NOT NULL,
482+ "name" text NOT NULL,
483+ "component_drug_ids" text[] DEFAULT '{}' NOT NULL,
484+ "modality" text DEFAULT 'drug_combination' NOT NULL,
485+ "description" text,
486+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
487+);
488+--> statement-breakpoint
489+CREATE TABLE "clinical_trials" (
490+ "id" varchar(32) PRIMARY KEY NOT NULL,
491+ "nct_id" text NOT NULL,
492+ "brief_title" text NOT NULL,
493+ "official_title" text,
494+ "acronym" text,
495+ "study_type" text,
496+ "phases" text[] DEFAULT '{}' NOT NULL,
497+ "overall_status" text,
498+ "why_stopped" text,
499+ "start_date" text,
500+ "primary_completion_date" text,
501+ "completion_date" text,
502+ "first_posted_date" text,
503+ "last_update_posted_date" text,
504+ "results_first_posted_date" text,
505+ "has_results" boolean DEFAULT false NOT NULL,
506+ "enrollment_count" integer,
507+ "enrollment_type" text,
508+ "lead_sponsor" text,
509+ "lead_sponsor_class" text,
510+ "collaborators" text[] DEFAULT '{}' NOT NULL,
511+ "conditions" text[] DEFAULT '{}' NOT NULL,
512+ "keywords" text[] DEFAULT '{}' NOT NULL,
513+ "interventions" jsonb DEFAULT '[]'::jsonb NOT NULL,
514+ "arms" jsonb DEFAULT '[]'::jsonb NOT NULL,
515+ "primary_outcomes" jsonb DEFAULT '[]'::jsonb NOT NULL,
516+ "secondary_outcomes" jsonb DEFAULT '[]'::jsonb NOT NULL,
517+ "eligibility" jsonb DEFAULT '{}'::jsonb NOT NULL,
518+ "sex" text,
519+ "minimum_age" text,
520+ "maximum_age" text,
521+ "countries" text[] DEFAULT '{}' NOT NULL,
522+ "locations_count" integer DEFAULT 0 NOT NULL,
523+ "references" jsonb DEFAULT '[]'::jsonb NOT NULL,
524+ "brief_summary" text,
525+ "is_oncology" boolean DEFAULT true NOT NULL,
526+ "source_record_id" integer,
527+ "ingest_run_id" text,
528+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
529+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
530+);
531+--> statement-breakpoint
532+CREATE TABLE "trial_conditions" (
533+ "id" bigserial PRIMARY KEY NOT NULL,
534+ "trial_id" varchar(32) NOT NULL,
535+ "condition_text" text NOT NULL,
536+ "normalized" text NOT NULL,
537+ "cancer_id" varchar(32),
538+ "match_type" text DEFAULT 'UNRESOLVED' NOT NULL,
539+ "confidence" real
540+);
541+--> statement-breakpoint
542+CREATE TABLE "trial_interventions" (
543+ "id" bigserial PRIMARY KEY NOT NULL,
544+ "trial_id" varchar(32) NOT NULL,
545+ "name" text NOT NULL,
546+ "normalized" text NOT NULL,
547+ "intervention_type" text,
548+ "drug_id" varchar(32),
549+ "match_type" text DEFAULT 'UNRESOLVED' NOT NULL
550+);
551+--> statement-breakpoint
552+CREATE TABLE "trial_locations" (
553+ "id" bigserial PRIMARY KEY NOT NULL,
554+ "trial_id" varchar(32) NOT NULL,
555+ "facility" text,
556+ "city" text,
557+ "state" text,
558+ "zip" text,
559+ "country" text,
560+ "status" text,
561+ "lat" real,
562+ "lng" real
563+);
564+--> statement-breakpoint
565+CREATE TABLE "trial_pulse" (
566+ "id" bigserial PRIMARY KEY NOT NULL,
567+ "day" date NOT NULL,
568+ "cancer_id" varchar(32),
569+ "phase" text,
570+ "new_trials" integer NOT NULL,
571+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
572+);
573+--> statement-breakpoint
574+CREATE TABLE "literature_counts" (
575+ "id" bigserial PRIMARY KEY NOT NULL,
576+ "cancer_id" varchar(32) NOT NULL,
577+ "window_key" text NOT NULL,
578+ "window_start" text,
579+ "window_end" text,
580+ "query" text NOT NULL,
581+ "count" integer NOT NULL,
582+ "provenance_id" integer NOT NULL,
583+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
584+);
585+--> statement-breakpoint
586+CREATE TABLE "publication_entity_edges" (
587+ "id" bigserial PRIMARY KEY NOT NULL,
588+ "publication_id" varchar(32) NOT NULL,
589+ "entity_type" text NOT NULL,
590+ "entity_id" text NOT NULL,
591+ "method" text NOT NULL,
592+ "confidence" real,
593+ "status" text DEFAULT 'candidate' NOT NULL,
594+ "source_id" varchar(32),
595+ "ingest_run_id" text,
596+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
597+);
598+--> statement-breakpoint
599+CREATE TABLE "publications" (
600+ "id" varchar(32) PRIMARY KEY NOT NULL,
601+ "pmid" text,
602+ "doi" text,
603+ "pmcid" text,
604+ "title" text NOT NULL,
605+ "abstract" text,
606+ "journal" text,
607+ "journal_iso" text,
608+ "pub_date" text,
609+ "pub_year" integer,
610+ "publication_types" text[] DEFAULT '{}' NOT NULL,
611+ "mesh_terms" jsonb DEFAULT '[]'::jsonb NOT NULL,
612+ "authors" jsonb DEFAULT '[]'::jsonb NOT NULL,
613+ "language" text,
614+ "is_preprint" boolean DEFAULT false NOT NULL,
615+ "retracted" boolean DEFAULT false NOT NULL,
616+ "retraction_notice" text,
617+ "nct_ids" text[] DEFAULT '{}' NOT NULL,
618+ "cited_by_count" integer,
619+ "source_record_id" integer,
620+ "ingest_run_id" text,
621+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
622+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
623+);
624+--> statement-breakpoint
625+CREATE TABLE "civic_evidence_items" (
626+ "id" bigserial PRIMARY KEY NOT NULL,
627+ "civic_id" integer NOT NULL,
628+ "name" text,
629+ "molecular_profile_id" integer,
630+ "molecular_profile_name" text,
631+ "gene_symbols" text[] DEFAULT '{}' NOT NULL,
632+ "gene_ids" text[] DEFAULT '{}' NOT NULL,
633+ "variant_ids" text[] DEFAULT '{}' NOT NULL,
634+ "civic_variant_ids" integer[] DEFAULT '{}' NOT NULL,
635+ "disease_name" text,
636+ "doid" text,
637+ "cancer_id" varchar(32),
638+ "cancer_match_type" text,
639+ "therapy_names" text[] DEFAULT '{}' NOT NULL,
640+ "therapy_ids" text[] DEFAULT '{}' NOT NULL,
641+ "therapy_interaction_type" text,
642+ "evidence_type" text,
643+ "evidence_level" text,
644+ "evidence_direction" text,
645+ "significance" text,
646+ "evidence_rating" integer,
647+ "status" text,
648+ "description" text,
649+ "pmid" text,
650+ "source_citation" text,
651+ "phenotypes" text[] DEFAULT '{}' NOT NULL,
652+ "provenance_id" integer NOT NULL,
653+ "ingest_run_id" text,
654+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
655+);
656+--> statement-breakpoint
657+CREATE TABLE "knowledge_edges" (
658+ "id" bigserial PRIMARY KEY NOT NULL,
659+ "source_entity_type" text NOT NULL,
660+ "source_entity_id" text NOT NULL,
661+ "target_entity_type" text NOT NULL,
662+ "target_entity_id" text NOT NULL,
663+ "relationship_type" text NOT NULL,
664+ "cancer_context_ids" text[] DEFAULT '{}' NOT NULL,
665+ "predictive" boolean,
666+ "prognostic" boolean,
667+ "diagnostic" boolean,
668+ "predisposing" boolean,
669+ "direction" text,
670+ "evidence_level" text,
671+ "evidence_score" real,
672+ "evidence_category" text DEFAULT 'curated_evidence' NOT NULL,
673+ "status" text DEFAULT 'active' NOT NULL,
674+ "source_id" varchar(32) NOT NULL,
675+ "source_record_id" text,
676+ "provenance_ids" integer[] DEFAULT '{}' NOT NULL,
677+ "support_count" integer DEFAULT 1 NOT NULL,
678+ "first_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
679+ "last_seen_at" timestamp with time zone DEFAULT now() NOT NULL
680+);
681+--> statement-breakpoint
682+CREATE TABLE "risk_factors" (
683+ "id" bigserial PRIMARY KEY NOT NULL,
684+ "slug" text NOT NULL,
685+ "name" text NOT NULL,
686+ "kind" text NOT NULL,
687+ "classification_authority" text,
688+ "classification" text,
689+ "description" text,
690+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
691+);
692+--> statement-breakpoint
693+CREATE TABLE "epidemiology_observations" (
694+ "id" bigserial PRIMARY KEY NOT NULL,
695+ "cancer_id" varchar(32) NOT NULL,
696+ "geography_id" varchar(32) NOT NULL,
697+ "year" integer NOT NULL,
698+ "year_end" integer,
699+ "sex" text DEFAULT 'all' NOT NULL,
700+ "age_group" text DEFAULT 'all' NOT NULL,
701+ "metric" text NOT NULL,
702+ "value" double precision NOT NULL,
703+ "unit" text NOT NULL,
704+ "lower_ci" double precision,
705+ "upper_ci" double precision,
706+ "standard_population" text,
707+ "estimate_type" text DEFAULT 'observed' NOT NULL,
708+ "site_definition" text,
709+ "source_id" varchar(32) NOT NULL,
710+ "provenance_id" integer NOT NULL,
711+ "ingest_run_id" text,
712+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
713+);
714+--> statement-breakpoint
715+CREATE TABLE "survival_observations" (
716+ "id" bigserial PRIMARY KEY NOT NULL,
717+ "cancer_id" varchar(32) NOT NULL,
718+ "geography_id" varchar(32),
719+ "stage" text,
720+ "staging_system" text,
721+ "sex" text DEFAULT 'all' NOT NULL,
722+ "age_group" text DEFAULT 'all' NOT NULL,
723+ "diagnosis_period" text,
724+ "survival_type" text NOT NULL,
725+ "duration_months" integer NOT NULL,
726+ "probability" real,
727+ "median_months" real,
728+ "cohort_size" integer,
729+ "lower_ci" real,
730+ "upper_ci" real,
731+ "method" text,
732+ "source_id" varchar(32) NOT NULL,
733+ "provenance_id" integer NOT NULL,
734+ "ingest_run_id" text,
735+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
736+);
737+--> statement-breakpoint
738+CREATE TABLE "ai_answers" (
739+ "id" bigserial PRIMARY KEY NOT NULL,
740+ "kind" text NOT NULL,
741+ "subject_id" text,
742+ "question_hash" text NOT NULL,
743+ "question" text,
744+ "answer" jsonb NOT NULL,
745+ "model" text NOT NULL,
746+ "prompt_version" text NOT NULL,
747+ "source_snapshot" jsonb DEFAULT '{}'::jsonb NOT NULL,
748+ "data_as_of" timestamp with time zone NOT NULL,
749+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
750+);
751+--> statement-breakpoint
752+CREATE TABLE "api_keys" (
753+ "id" bigserial PRIMARY KEY NOT NULL,
754+ "key_hash" text NOT NULL,
755+ "prefix" text NOT NULL,
756+ "label" text,
757+ "owner_email" text,
758+ "tier" text DEFAULT 'free' NOT NULL,
759+ "rate_limit_per_minute" integer DEFAULT 60 NOT NULL,
760+ "active" boolean DEFAULT true NOT NULL,
761+ "last_used_at" timestamp with time zone,
762+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
763+);
764+--> statement-breakpoint
765+CREATE TABLE "entity_counters" (
766+ "id" bigserial PRIMARY KEY NOT NULL,
767+ "entity_type" text NOT NULL,
768+ "entity_id" text NOT NULL,
769+ "trial_count" integer DEFAULT 0 NOT NULL,
770+ "active_trial_count" integer DEFAULT 0 NOT NULL,
771+ "recruiting_trial_count" integer DEFAULT 0 NOT NULL,
772+ "phase3_trial_count" integer DEFAULT 0 NOT NULL,
773+ "publication_count" integer DEFAULT 0 NOT NULL,
774+ "publication_count_5y" integer DEFAULT 0 NOT NULL,
775+ "publication_count_12m" integer DEFAULT 0 NOT NULL,
776+ "gene_count" integer DEFAULT 0 NOT NULL,
777+ "variant_count" integer DEFAULT 0 NOT NULL,
778+ "drug_count" integer DEFAULT 0 NOT NULL,
779+ "approved_drug_count" integer DEFAULT 0 NOT NULL,
780+ "evidence_count" integer DEFAULT 0 NOT NULL,
781+ "cohort_count" integer DEFAULT 0 NOT NULL,
782+ "subtype_count" integer DEFAULT 0 NOT NULL,
783+ "descendant_count" integer DEFAULT 0 NOT NULL,
784+ "epidemiology_obs_count" integer DEFAULT 0 NOT NULL,
785+ "survival_obs_count" integer DEFAULT 0 NOT NULL,
786+ "completeness" jsonb DEFAULT '{}'::jsonb NOT NULL,
787+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
788+);
789+--> statement-breakpoint
790+CREATE TABLE "metric_definitions" (
791+ "id" varchar(32) PRIMARY KEY NOT NULL,
792+ "slug" text NOT NULL,
793+ "name" text NOT NULL,
794+ "description" text NOT NULL,
795+ "formula" text NOT NULL,
796+ "formula_version" text NOT NULL,
797+ "unit" text NOT NULL,
798+ "higher_is_worse" boolean,
799+ "aggregation" text,
800+ "valid_dimensions" text[] DEFAULT '{}' NOT NULL,
801+ "source_slugs" text[] DEFAULT '{}' NOT NULL,
802+ "category" text NOT NULL,
803+ "eligibility" jsonb DEFAULT '{}'::jsonb NOT NULL,
804+ "experimental" boolean DEFAULT false NOT NULL,
805+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
806+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
807+);
808+--> statement-breakpoint
809+CREATE TABLE "ranking_snapshots" (
810+ "id" bigserial PRIMARY KEY NOT NULL,
811+ "metric_id" varchar(32) NOT NULL,
812+ "metric_slug" text NOT NULL,
813+ "scope_key" text NOT NULL,
814+ "geography" text DEFAULT 'WORLD' NOT NULL,
815+ "sex" text DEFAULT 'all' NOT NULL,
816+ "age_group" text DEFAULT 'all' NOT NULL,
817+ "year" integer,
818+ "entity_level" text DEFAULT 'top' NOT NULL,
819+ "formula_version" text NOT NULL,
820+ "eligible_entities" integer NOT NULL,
821+ "inputs_hash" text NOT NULL,
822+ "source_ids" text[] DEFAULT '{}' NOT NULL,
823+ "is_current" boolean DEFAULT true NOT NULL,
824+ "generated_at" timestamp with time zone DEFAULT now() NOT NULL
825+);
826+--> statement-breakpoint
827+CREATE TABLE "rankings" (
828+ "id" bigserial PRIMARY KEY NOT NULL,
829+ "snapshot_id" integer NOT NULL,
830+ "metric_slug" text NOT NULL,
831+ "scope_key" text NOT NULL,
832+ "cancer_id" varchar(32) NOT NULL,
833+ "rank" integer NOT NULL,
834+ "eligible_entities" integer NOT NULL,
835+ "percentile" real NOT NULL,
836+ "value" double precision NOT NULL,
837+ "unit" text NOT NULL,
838+ "confidence" text DEFAULT 'MEDIUM' NOT NULL,
839+ "inputs" jsonb DEFAULT '{}'::jsonb NOT NULL,
840+ "breakdown" jsonb,
841+ "previous_rank" integer,
842+ "generated_at" timestamp with time zone DEFAULT now() NOT NULL
843+);
844+--> statement-breakpoint
845+CREATE INDEX "change_events_entity_idx" ON "change_events" USING btree ("entity_type","entity_id","created_at");--> statement-breakpoint
846+CREATE UNIQUE INDEX "connector_field_stats_uq" ON "connector_field_stats" USING btree ("connector_id","entity","field");--> statement-breakpoint
847+CREATE INDEX "ingest_runs_connector_idx" ON "ingest_runs" USING btree ("connector_id","started_at");--> statement-breakpoint
848+CREATE INDEX "provenance_source_idx" ON "provenance" USING btree ("source_id","source_record_id");--> statement-breakpoint
849+CREATE INDEX "provenance_pmid_idx" ON "provenance" USING btree ("pmid");--> statement-breakpoint
850+CREATE UNIQUE INDEX "source_records_uq" ON "source_records" USING btree ("source_id","entity_kind","source_record_id");--> statement-breakpoint
851+CREATE INDEX "source_records_canonical_idx" ON "source_records" USING btree ("canonical_type","canonical_id");--> statement-breakpoint
852+CREATE UNIQUE INDEX "sources_slug_uq" ON "sources" USING btree ("slug");--> statement-breakpoint
853+CREATE UNIQUE INDEX "unresolved_labels_uq" ON "unresolved_labels" USING btree ("source_id","entity_kind","normalized");--> statement-breakpoint
854+CREATE INDEX "unresolved_labels_count_idx" ON "unresolved_labels" USING btree ("status","count");--> statement-breakpoint
855+CREATE UNIQUE INDEX "anatomical_sites_slug_uq" ON "anatomical_sites" USING btree ("slug");--> statement-breakpoint
856+CREATE UNIQUE INDEX "cancer_aliases_uq" ON "cancer_aliases" USING btree ("cancer_id","normalized","alias_type");--> statement-breakpoint
857+CREATE INDEX "cancer_aliases_norm_idx" ON "cancer_aliases" USING btree ("normalized");--> statement-breakpoint
858+CREATE UNIQUE INDEX "cancer_anatomy_uq" ON "cancer_anatomy" USING btree ("cancer_id","site_id","relation");--> statement-breakpoint
859+CREATE UNIQUE INDEX "cancer_codes_uq" ON "cancer_codes" USING btree ("cancer_id","system","code");--> statement-breakpoint
860+CREATE INDEX "cancer_codes_lookup_idx" ON "cancer_codes" USING btree ("system","code");--> statement-breakpoint
861+CREATE UNIQUE INDEX "cancer_hierarchy_uq" ON "cancer_hierarchy" USING btree ("parent_id","child_id","hierarchy_type");--> statement-breakpoint
862+CREATE INDEX "cancer_hierarchy_child_idx" ON "cancer_hierarchy" USING btree ("child_id");--> statement-breakpoint
863+CREATE UNIQUE INDEX "cancers_slug_uq" ON "cancers" USING btree ("slug");--> statement-breakpoint
864+CREATE UNIQUE INDEX "cancers_ncit_uq" ON "cancers" USING btree ("primary_ncit_code");--> statement-breakpoint
865+CREATE INDEX "cancers_name_idx" ON "cancers" USING btree ("canonical_name");--> statement-breakpoint
866+CREATE INDEX "cancers_type_idx" ON "cancers" USING btree ("entity_type","malignant","top_level");--> statement-breakpoint
867+CREATE UNIQUE INDEX "geographies_slug_uq" ON "geographies" USING btree ("slug");--> statement-breakpoint
868+CREATE INDEX "geographies_iso3_idx" ON "geographies" USING btree ("iso3");--> statement-breakpoint
869+CREATE UNIQUE INDEX "biomarkers_slug_uq" ON "biomarkers" USING btree ("slug");--> statement-breakpoint
870+CREATE UNIQUE INDEX "cancer_gene_freq_uq" ON "cancer_gene_frequencies" USING btree ("cohort_id","gene_symbol","alteration_type");--> statement-breakpoint
871+CREATE INDEX "cancer_gene_freq_cancer_idx" ON "cancer_gene_frequencies" USING btree ("cancer_id","frequency");--> statement-breakpoint
872+CREATE INDEX "cancer_gene_freq_gene_idx" ON "cancer_gene_frequencies" USING btree ("gene_id");--> statement-breakpoint
873+CREATE UNIQUE INDEX "entity_embeddings_uq" ON "entity_embeddings" USING btree ("entity_type","entity_id","model");--> statement-breakpoint
874+CREATE UNIQUE INDEX "gene_aliases_uq" ON "gene_aliases" USING btree ("gene_id","alias","alias_type");--> statement-breakpoint
875+CREATE INDEX "gene_aliases_alias_idx" ON "gene_aliases" USING btree ("alias");--> statement-breakpoint
876+CREATE UNIQUE INDEX "genes_symbol_uq" ON "genes" USING btree ("symbol");--> statement-breakpoint
877+CREATE UNIQUE INDEX "genes_hgnc_uq" ON "genes" USING btree ("hgnc_id");--> statement-breakpoint
878+CREATE INDEX "genes_ensembl_idx" ON "genes" USING btree ("ensembl_gene_id");--> statement-breakpoint
879+CREATE INDEX "genes_ncbi_idx" ON "genes" USING btree ("ncbi_gene_id");--> statement-breakpoint
880+CREATE UNIQUE INDEX "genomic_cohorts_uq" ON "genomic_cohorts" USING btree ("source_id","study_id");--> statement-breakpoint
881+CREATE INDEX "genomic_cohorts_cancer_idx" ON "genomic_cohorts" USING btree ("cancer_id");--> statement-breakpoint
882+CREATE UNIQUE INDEX "variant_aliases_uq" ON "variant_aliases" USING btree ("variant_id","alias");--> statement-breakpoint
883+CREATE UNIQUE INDEX "variant_clinsig_uq" ON "variant_clinical_significance" USING btree ("clinvar_variation_id");--> statement-breakpoint
884+CREATE UNIQUE INDEX "variants_slug_uq" ON "variants" USING btree ("slug");--> statement-breakpoint
885+CREATE INDEX "variants_gene_idx" ON "variants" USING btree ("gene_id");--> statement-breakpoint
886+CREATE INDEX "variants_clinvar_idx" ON "variants" USING btree ("clinvar_variation_id");--> statement-breakpoint
887+CREATE INDEX "variants_civic_idx" ON "variants" USING btree ("civic_variant_id");--> statement-breakpoint
888+CREATE UNIQUE INDEX "drug_aliases_uq" ON "drug_aliases" USING btree ("drug_id","normalized","alias_type");--> statement-breakpoint
889+CREATE INDEX "drug_aliases_norm_idx" ON "drug_aliases" USING btree ("normalized");--> statement-breakpoint
890+CREATE INDEX "drug_approvals_drug_idx" ON "drug_approvals" USING btree ("drug_id");--> statement-breakpoint
891+CREATE INDEX "drug_approvals_cancer_idx" ON "drug_approvals" USING btree ("cancer_id");--> statement-breakpoint
892+CREATE UNIQUE INDEX "drugs_slug_uq" ON "drugs" USING btree ("slug");--> statement-breakpoint
893+CREATE INDEX "drugs_ncit_idx" ON "drugs" USING btree ("ncit_code");--> statement-breakpoint
894+CREATE INDEX "drugs_civic_idx" ON "drugs" USING btree ("civic_therapy_id");--> statement-breakpoint
895+CREATE INDEX "drugs_chembl_idx" ON "drugs" USING btree ("chembl_id");--> statement-breakpoint
896+CREATE UNIQUE INDEX "treatment_regimens_slug_uq" ON "treatment_regimens" USING btree ("slug");--> statement-breakpoint
897+CREATE UNIQUE INDEX "clinical_trials_nct_uq" ON "clinical_trials" USING btree ("nct_id");--> statement-breakpoint
898+CREATE INDEX "clinical_trials_status_idx" ON "clinical_trials" USING btree ("overall_status");--> statement-breakpoint
899+CREATE INDEX "clinical_trials_updated_idx" ON "clinical_trials" USING btree ("last_update_posted_date");--> statement-breakpoint
900+CREATE INDEX "clinical_trials_sponsor_idx" ON "clinical_trials" USING btree ("lead_sponsor");--> statement-breakpoint
901+CREATE UNIQUE INDEX "trial_conditions_uq" ON "trial_conditions" USING btree ("trial_id","normalized");--> statement-breakpoint
902+CREATE INDEX "trial_conditions_cancer_idx" ON "trial_conditions" USING btree ("cancer_id");--> statement-breakpoint
903+CREATE INDEX "trial_conditions_norm_idx" ON "trial_conditions" USING btree ("normalized");--> statement-breakpoint
904+CREATE UNIQUE INDEX "trial_interventions_uq" ON "trial_interventions" USING btree ("trial_id","normalized");--> statement-breakpoint
905+CREATE INDEX "trial_interventions_drug_idx" ON "trial_interventions" USING btree ("drug_id");--> statement-breakpoint
906+CREATE INDEX "trial_locations_trial_idx" ON "trial_locations" USING btree ("trial_id");--> statement-breakpoint
907+CREATE INDEX "trial_locations_country_idx" ON "trial_locations" USING btree ("country");--> statement-breakpoint
908+CREATE UNIQUE INDEX "trial_pulse_uq" ON "trial_pulse" USING btree ("day","cancer_id","phase");--> statement-breakpoint
909+CREATE UNIQUE INDEX "literature_counts_uq" ON "literature_counts" USING btree ("cancer_id","window_key");--> statement-breakpoint
910+CREATE INDEX "literature_counts_window_idx" ON "literature_counts" USING btree ("window_key","count");--> statement-breakpoint
911+CREATE UNIQUE INDEX "pub_entity_edges_uq" ON "publication_entity_edges" USING btree ("publication_id","entity_type","entity_id","method");--> statement-breakpoint
912+CREATE INDEX "pub_entity_edges_entity_idx" ON "publication_entity_edges" USING btree ("entity_type","entity_id");--> statement-breakpoint
913+CREATE UNIQUE INDEX "publications_pmid_uq" ON "publications" USING btree ("pmid");--> statement-breakpoint
914+CREATE INDEX "publications_doi_idx" ON "publications" USING btree ("doi");--> statement-breakpoint
915+CREATE INDEX "publications_year_idx" ON "publications" USING btree ("pub_year");--> statement-breakpoint
916+CREATE INDEX "publications_retracted_idx" ON "publications" USING btree ("retracted");--> statement-breakpoint
917+CREATE UNIQUE INDEX "civic_evidence_uq" ON "civic_evidence_items" USING btree ("civic_id");--> statement-breakpoint
918+CREATE INDEX "civic_evidence_cancer_idx" ON "civic_evidence_items" USING btree ("cancer_id");--> statement-breakpoint
919+CREATE INDEX "civic_evidence_gene_idx" ON "civic_evidence_items" USING btree ("gene_symbols");--> statement-breakpoint
920+CREATE UNIQUE INDEX "knowledge_edges_uq" ON "knowledge_edges" USING btree ("source_entity_type","source_entity_id","target_entity_type","target_entity_id","relationship_type","source_id","source_record_id");--> statement-breakpoint
921+CREATE INDEX "knowledge_edges_source_idx" ON "knowledge_edges" USING btree ("source_entity_type","source_entity_id","relationship_type");--> statement-breakpoint
922+CREATE INDEX "knowledge_edges_target_idx" ON "knowledge_edges" USING btree ("target_entity_type","target_entity_id","relationship_type");--> statement-breakpoint
923+CREATE UNIQUE INDEX "risk_factors_slug_uq" ON "risk_factors" USING btree ("slug");--> statement-breakpoint
924+CREATE UNIQUE INDEX "epi_obs_uq" ON "epidemiology_observations" USING btree ("cancer_id","geography_id","year","sex","age_group","metric","source_id","site_definition");--> statement-breakpoint
925+CREATE INDEX "epi_obs_lookup_idx" ON "epidemiology_observations" USING btree ("metric","geography_id","year","sex");--> statement-breakpoint
926+CREATE INDEX "epi_obs_cancer_idx" ON "epidemiology_observations" USING btree ("cancer_id","metric");--> statement-breakpoint
927+CREATE INDEX "survival_obs_cancer_idx" ON "survival_observations" USING btree ("cancer_id","survival_type","duration_months");--> statement-breakpoint
928+CREATE UNIQUE INDEX "ai_answers_uq" ON "ai_answers" USING btree ("kind","question_hash","prompt_version");--> statement-breakpoint
929+CREATE UNIQUE INDEX "api_keys_hash_uq" ON "api_keys" USING btree ("key_hash");--> statement-breakpoint
930+CREATE UNIQUE INDEX "entity_counters_uq" ON "entity_counters" USING btree ("entity_type","entity_id");--> statement-breakpoint
931+CREATE INDEX "entity_counters_trials_idx" ON "entity_counters" USING btree ("entity_type","active_trial_count");--> statement-breakpoint
932+CREATE UNIQUE INDEX "metric_definitions_slug_uq" ON "metric_definitions" USING btree ("slug");--> statement-breakpoint
933+CREATE INDEX "ranking_snapshots_lookup_idx" ON "ranking_snapshots" USING btree ("metric_slug","scope_key","is_current");--> statement-breakpoint
934+CREATE UNIQUE INDEX "rankings_uq" ON "rankings" USING btree ("snapshot_id","cancer_id");--> statement-breakpoint
935+CREATE INDEX "rankings_cancer_idx" ON "rankings" USING btree ("cancer_id","metric_slug");--> statement-breakpoint
936+CREATE INDEX "rankings_lookup_idx" ON "rankings" USING btree ("metric_slug","scope_key","rank");
\ No newline at end of file
added packages/database/migrations/meta/0000_snapshot.json +6918 −0
@@ -0,0 +1,6918 @@
1+{
2+ "id": "7cee03f3-3bb2-47c2-b164-ea15161ffedd",
3+ "prevId": "00000000-0000-0000-0000-000000000000",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.audit_log": {
8+ "name": "audit_log",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "bigserial",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "actor": {
18+ "name": "actor",
19+ "type": "text",
20+ "primaryKey": false,
21+ "notNull": true
22+ },
23+ "action": {
24+ "name": "action",
25+ "type": "text",
26+ "primaryKey": false,
27+ "notNull": true
28+ },
29+ "entity_type": {
30+ "name": "entity_type",
31+ "type": "text",
32+ "primaryKey": false,
33+ "notNull": false
34+ },
35+ "entity_id": {
36+ "name": "entity_id",
37+ "type": "text",
38+ "primaryKey": false,
39+ "notNull": false
40+ },
41+ "before": {
42+ "name": "before",
43+ "type": "jsonb",
44+ "primaryKey": false,
45+ "notNull": false
46+ },
47+ "after": {
48+ "name": "after",
49+ "type": "jsonb",
50+ "primaryKey": false,
51+ "notNull": false
52+ },
53+ "reason": {
54+ "name": "reason",
55+ "type": "text",
56+ "primaryKey": false,
57+ "notNull": false
58+ },
59+ "created_at": {
60+ "name": "created_at",
61+ "type": "timestamp with time zone",
62+ "primaryKey": false,
63+ "notNull": true,
64+ "default": "now()"
65+ }
66+ },
67+ "indexes": {},
68+ "foreignKeys": {},
69+ "compositePrimaryKeys": {},
70+ "uniqueConstraints": {},
71+ "policies": {},
72+ "checkConstraints": {},
73+ "isRLSEnabled": false
74+ },
75+ "public.change_events": {
76+ "name": "change_events",
77+ "schema": "",
78+ "columns": {
79+ "id": {
80+ "name": "id",
81+ "type": "bigserial",
82+ "primaryKey": true,
83+ "notNull": true
84+ },
85+ "entity_type": {
86+ "name": "entity_type",
87+ "type": "text",
88+ "primaryKey": false,
89+ "notNull": true
90+ },
91+ "entity_id": {
92+ "name": "entity_id",
93+ "type": "text",
94+ "primaryKey": false,
95+ "notNull": true
96+ },
97+ "kind": {
98+ "name": "kind",
99+ "type": "text",
100+ "primaryKey": false,
101+ "notNull": true
102+ },
103+ "summary": {
104+ "name": "summary",
105+ "type": "text",
106+ "primaryKey": false,
107+ "notNull": true
108+ },
109+ "before": {
110+ "name": "before",
111+ "type": "jsonb",
112+ "primaryKey": false,
113+ "notNull": false
114+ },
115+ "after": {
116+ "name": "after",
117+ "type": "jsonb",
118+ "primaryKey": false,
119+ "notNull": false
120+ },
121+ "ingest_run_id": {
122+ "name": "ingest_run_id",
123+ "type": "text",
124+ "primaryKey": false,
125+ "notNull": false
126+ },
127+ "created_at": {
128+ "name": "created_at",
129+ "type": "timestamp with time zone",
130+ "primaryKey": false,
131+ "notNull": true,
132+ "default": "now()"
133+ }
134+ },
135+ "indexes": {
136+ "change_events_entity_idx": {
137+ "name": "change_events_entity_idx",
138+ "columns": [
139+ {
140+ "expression": "entity_type",
141+ "isExpression": false,
142+ "asc": true,
143+ "nulls": "last"
144+ },
145+ {
146+ "expression": "entity_id",
147+ "isExpression": false,
148+ "asc": true,
149+ "nulls": "last"
150+ },
151+ {
152+ "expression": "created_at",
153+ "isExpression": false,
154+ "asc": true,
155+ "nulls": "last"
156+ }
157+ ],
158+ "isUnique": false,
159+ "concurrently": false,
160+ "method": "btree",
161+ "with": {}
162+ }
163+ },
164+ "foreignKeys": {},
165+ "compositePrimaryKeys": {},
166+ "uniqueConstraints": {},
167+ "policies": {},
168+ "checkConstraints": {},
169+ "isRLSEnabled": false
170+ },
171+ "public.connector_cursors": {
172+ "name": "connector_cursors",
173+ "schema": "",
174+ "columns": {
175+ "connector_id": {
176+ "name": "connector_id",
177+ "type": "text",
178+ "primaryKey": true,
179+ "notNull": true
180+ },
181+ "cursor": {
182+ "name": "cursor",
183+ "type": "jsonb",
184+ "primaryKey": false,
185+ "notNull": true,
186+ "default": "'{}'::jsonb"
187+ },
188+ "last_success_at": {
189+ "name": "last_success_at",
190+ "type": "timestamp with time zone",
191+ "primaryKey": false,
192+ "notNull": false
193+ },
194+ "last_attempt_at": {
195+ "name": "last_attempt_at",
196+ "type": "timestamp with time zone",
197+ "primaryKey": false,
198+ "notNull": false
199+ },
200+ "paused": {
201+ "name": "paused",
202+ "type": "boolean",
203+ "primaryKey": false,
204+ "notNull": true,
205+ "default": false
206+ },
207+ "health": {
208+ "name": "health",
209+ "type": "text",
210+ "primaryKey": false,
211+ "notNull": true,
212+ "default": "'unknown'"
213+ },
214+ "health_detail": {
215+ "name": "health_detail",
216+ "type": "text",
217+ "primaryKey": false,
218+ "notNull": false
219+ },
220+ "updated_at": {
221+ "name": "updated_at",
222+ "type": "timestamp with time zone",
223+ "primaryKey": false,
224+ "notNull": true,
225+ "default": "now()"
226+ }
227+ },
228+ "indexes": {},
229+ "foreignKeys": {},
230+ "compositePrimaryKeys": {},
231+ "uniqueConstraints": {},
232+ "policies": {},
233+ "checkConstraints": {},
234+ "isRLSEnabled": false
235+ },
236+ "public.connector_field_stats": {
237+ "name": "connector_field_stats",
238+ "schema": "",
239+ "columns": {
240+ "id": {
241+ "name": "id",
242+ "type": "bigserial",
243+ "primaryKey": true,
244+ "notNull": true
245+ },
246+ "connector_id": {
247+ "name": "connector_id",
248+ "type": "text",
249+ "primaryKey": false,
250+ "notNull": true
251+ },
252+ "entity": {
253+ "name": "entity",
254+ "type": "text",
255+ "primaryKey": false,
256+ "notNull": true
257+ },
258+ "field": {
259+ "name": "field",
260+ "type": "text",
261+ "primaryKey": false,
262+ "notNull": true
263+ },
264+ "types": {
265+ "name": "types",
266+ "type": "text[]",
267+ "primaryKey": false,
268+ "notNull": true,
269+ "default": "'{}'"
270+ },
271+ "seen_count": {
272+ "name": "seen_count",
273+ "type": "integer",
274+ "primaryKey": false,
275+ "notNull": true,
276+ "default": 0
277+ },
278+ "null_count": {
279+ "name": "null_count",
280+ "type": "integer",
281+ "primaryKey": false,
282+ "notNull": true,
283+ "default": 0
284+ },
285+ "first_seen_run": {
286+ "name": "first_seen_run",
287+ "type": "text",
288+ "primaryKey": false,
289+ "notNull": false
290+ },
291+ "last_seen_run": {
292+ "name": "last_seen_run",
293+ "type": "text",
294+ "primaryKey": false,
295+ "notNull": false
296+ },
297+ "updated_at": {
298+ "name": "updated_at",
299+ "type": "timestamp with time zone",
300+ "primaryKey": false,
301+ "notNull": true,
302+ "default": "now()"
303+ }
304+ },
305+ "indexes": {
306+ "connector_field_stats_uq": {
307+ "name": "connector_field_stats_uq",
308+ "columns": [
309+ {
310+ "expression": "connector_id",
311+ "isExpression": false,
312+ "asc": true,
313+ "nulls": "last"
314+ },
315+ {
316+ "expression": "entity",
317+ "isExpression": false,
318+ "asc": true,
319+ "nulls": "last"
320+ },
321+ {
322+ "expression": "field",
323+ "isExpression": false,
324+ "asc": true,
325+ "nulls": "last"
326+ }
327+ ],
328+ "isUnique": true,
329+ "concurrently": false,
330+ "method": "btree",
331+ "with": {}
332+ }
333+ },
334+ "foreignKeys": {},
335+ "compositePrimaryKeys": {},
336+ "uniqueConstraints": {},
337+ "policies": {},
338+ "checkConstraints": {},
339+ "isRLSEnabled": false
340+ },
341+ "public.entity_merges": {
342+ "name": "entity_merges",
343+ "schema": "",
344+ "columns": {
345+ "id": {
346+ "name": "id",
347+ "type": "bigserial",
348+ "primaryKey": true,
349+ "notNull": true
350+ },
351+ "entity_type": {
352+ "name": "entity_type",
353+ "type": "text",
354+ "primaryKey": false,
355+ "notNull": true
356+ },
357+ "keep_id": {
358+ "name": "keep_id",
359+ "type": "text",
360+ "primaryKey": false,
361+ "notNull": true
362+ },
363+ "merge_id": {
364+ "name": "merge_id",
365+ "type": "text",
366+ "primaryKey": false,
367+ "notNull": true
368+ },
369+ "evidence": {
370+ "name": "evidence",
371+ "type": "jsonb",
372+ "primaryKey": false,
373+ "notNull": true,
374+ "default": "'{}'::jsonb"
375+ },
376+ "status": {
377+ "name": "status",
378+ "type": "text",
379+ "primaryKey": false,
380+ "notNull": true,
381+ "default": "'proposed'"
382+ },
383+ "decided_by": {
384+ "name": "decided_by",
385+ "type": "text",
386+ "primaryKey": false,
387+ "notNull": false
388+ },
389+ "decided_at": {
390+ "name": "decided_at",
391+ "type": "timestamp with time zone",
392+ "primaryKey": false,
393+ "notNull": false
394+ },
395+ "created_at": {
396+ "name": "created_at",
397+ "type": "timestamp with time zone",
398+ "primaryKey": false,
399+ "notNull": true,
400+ "default": "now()"
401+ }
402+ },
403+ "indexes": {},
404+ "foreignKeys": {},
405+ "compositePrimaryKeys": {},
406+ "uniqueConstraints": {},
407+ "policies": {},
408+ "checkConstraints": {},
409+ "isRLSEnabled": false
410+ },
411+ "public.id_sequences": {
412+ "name": "id_sequences",
413+ "schema": "",
414+ "columns": {
415+ "namespace": {
416+ "name": "namespace",
417+ "type": "varchar(16)",
418+ "primaryKey": true,
419+ "notNull": true
420+ },
421+ "next": {
422+ "name": "next",
423+ "type": "bigint",
424+ "primaryKey": false,
425+ "notNull": true,
426+ "default": 1
427+ }
428+ },
429+ "indexes": {},
430+ "foreignKeys": {},
431+ "compositePrimaryKeys": {},
432+ "uniqueConstraints": {},
433+ "policies": {},
434+ "checkConstraints": {},
435+ "isRLSEnabled": false
436+ },
437+ "public.ingest_runs": {
438+ "name": "ingest_runs",
439+ "schema": "",
440+ "columns": {
441+ "id": {
442+ "name": "id",
443+ "type": "text",
444+ "primaryKey": true,
445+ "notNull": true
446+ },
447+ "connector_id": {
448+ "name": "connector_id",
449+ "type": "text",
450+ "primaryKey": false,
451+ "notNull": true
452+ },
453+ "source_id": {
454+ "name": "source_id",
455+ "type": "varchar(32)",
456+ "primaryKey": false,
457+ "notNull": true
458+ },
459+ "mode": {
460+ "name": "mode",
461+ "type": "text",
462+ "primaryKey": false,
463+ "notNull": true,
464+ "default": "'incremental'"
465+ },
466+ "status": {
467+ "name": "status",
468+ "type": "text",
469+ "primaryKey": false,
470+ "notNull": true,
471+ "default": "'running'"
472+ },
473+ "started_at": {
474+ "name": "started_at",
475+ "type": "timestamp with time zone",
476+ "primaryKey": false,
477+ "notNull": true,
478+ "default": "now()"
479+ },
480+ "finished_at": {
481+ "name": "finished_at",
482+ "type": "timestamp with time zone",
483+ "primaryKey": false,
484+ "notNull": false
485+ },
486+ "duration_ms": {
487+ "name": "duration_ms",
488+ "type": "integer",
489+ "primaryKey": false,
490+ "notNull": false
491+ },
492+ "records_fetched": {
493+ "name": "records_fetched",
494+ "type": "integer",
495+ "primaryKey": false,
496+ "notNull": true,
497+ "default": 0
498+ },
499+ "records_created": {
500+ "name": "records_created",
501+ "type": "integer",
502+ "primaryKey": false,
503+ "notNull": true,
504+ "default": 0
505+ },
506+ "records_updated": {
507+ "name": "records_updated",
508+ "type": "integer",
509+ "primaryKey": false,
510+ "notNull": true,
511+ "default": 0
512+ },
513+ "records_unchanged": {
514+ "name": "records_unchanged",
515+ "type": "integer",
516+ "primaryKey": false,
517+ "notNull": true,
518+ "default": 0
519+ },
520+ "records_rejected": {
521+ "name": "records_rejected",
522+ "type": "integer",
523+ "primaryKey": false,
524+ "notNull": true,
525+ "default": 0
526+ },
527+ "http_requests": {
528+ "name": "http_requests",
529+ "type": "integer",
530+ "primaryKey": false,
531+ "notNull": true,
532+ "default": 0
533+ },
534+ "http_failures": {
535+ "name": "http_failures",
536+ "type": "integer",
537+ "primaryKey": false,
538+ "notNull": true,
539+ "default": 0
540+ },
541+ "rate_limit_events": {
542+ "name": "rate_limit_events",
543+ "type": "integer",
544+ "primaryKey": false,
545+ "notNull": true,
546+ "default": 0
547+ },
548+ "validation_failures": {
549+ "name": "validation_failures",
550+ "type": "integer",
551+ "primaryKey": false,
552+ "notNull": true,
553+ "default": 0
554+ },
555+ "schema_drift": {
556+ "name": "schema_drift",
557+ "type": "jsonb",
558+ "primaryKey": false,
559+ "notNull": true,
560+ "default": "'[]'::jsonb"
561+ },
562+ "cursor_before": {
563+ "name": "cursor_before",
564+ "type": "jsonb",
565+ "primaryKey": false,
566+ "notNull": false
567+ },
568+ "cursor_after": {
569+ "name": "cursor_after",
570+ "type": "jsonb",
571+ "primaryKey": false,
572+ "notNull": false
573+ },
574+ "error": {
575+ "name": "error",
576+ "type": "text",
577+ "primaryKey": false,
578+ "notNull": false
579+ },
580+ "log": {
581+ "name": "log",
582+ "type": "jsonb",
583+ "primaryKey": false,
584+ "notNull": true,
585+ "default": "'[]'::jsonb"
586+ },
587+ "dataset_version": {
588+ "name": "dataset_version",
589+ "type": "text",
590+ "primaryKey": false,
591+ "notNull": false
592+ },
593+ "anomaly": {
594+ "name": "anomaly",
595+ "type": "text",
596+ "primaryKey": false,
597+ "notNull": false
598+ }
599+ },
600+ "indexes": {
601+ "ingest_runs_connector_idx": {
602+ "name": "ingest_runs_connector_idx",
603+ "columns": [
604+ {
605+ "expression": "connector_id",
606+ "isExpression": false,
607+ "asc": true,
608+ "nulls": "last"
609+ },
610+ {
611+ "expression": "started_at",
612+ "isExpression": false,
613+ "asc": true,
614+ "nulls": "last"
615+ }
616+ ],
617+ "isUnique": false,
618+ "concurrently": false,
619+ "method": "btree",
620+ "with": {}
621+ }
622+ },
623+ "foreignKeys": {},
624+ "compositePrimaryKeys": {},
625+ "uniqueConstraints": {},
626+ "policies": {},
627+ "checkConstraints": {},
628+ "isRLSEnabled": false
629+ },
630+ "public.provenance": {
631+ "name": "provenance",
632+ "schema": "",
633+ "columns": {
634+ "id": {
635+ "name": "id",
636+ "type": "bigserial",
637+ "primaryKey": true,
638+ "notNull": true
639+ },
640+ "public_id": {
641+ "name": "public_id",
642+ "type": "varchar(32)",
643+ "primaryKey": false,
644+ "notNull": false
645+ },
646+ "source_id": {
647+ "name": "source_id",
648+ "type": "varchar(32)",
649+ "primaryKey": false,
650+ "notNull": true
651+ },
652+ "source_record_id": {
653+ "name": "source_record_id",
654+ "type": "text",
655+ "primaryKey": false,
656+ "notNull": false
657+ },
658+ "source_url": {
659+ "name": "source_url",
660+ "type": "text",
661+ "primaryKey": false,
662+ "notNull": false
663+ },
664+ "dataset": {
665+ "name": "dataset",
666+ "type": "text",
667+ "primaryKey": false,
668+ "notNull": false
669+ },
670+ "dataset_version": {
671+ "name": "dataset_version",
672+ "type": "text",
673+ "primaryKey": false,
674+ "notNull": false
675+ },
676+ "publication_id": {
677+ "name": "publication_id",
678+ "type": "varchar(32)",
679+ "primaryKey": false,
680+ "notNull": false
681+ },
682+ "pmid": {
683+ "name": "pmid",
684+ "type": "text",
685+ "primaryKey": false,
686+ "notNull": false
687+ },
688+ "doi": {
689+ "name": "doi",
690+ "type": "text",
691+ "primaryKey": false,
692+ "notNull": false
693+ },
694+ "retrieved_at": {
695+ "name": "retrieved_at",
696+ "type": "timestamp with time zone",
697+ "primaryKey": false,
698+ "notNull": true
699+ },
700+ "published_at": {
701+ "name": "published_at",
702+ "type": "text",
703+ "primaryKey": false,
704+ "notNull": false
705+ },
706+ "updated_at_source": {
707+ "name": "updated_at_source",
708+ "type": "text",
709+ "primaryKey": false,
710+ "notNull": false
711+ },
712+ "geography": {
713+ "name": "geography",
714+ "type": "text",
715+ "primaryKey": false,
716+ "notNull": false
717+ },
718+ "population": {
719+ "name": "population",
720+ "type": "text",
721+ "primaryKey": false,
722+ "notNull": false
723+ },
724+ "cohort_size": {
725+ "name": "cohort_size",
726+ "type": "integer",
727+ "primaryKey": false,
728+ "notNull": false
729+ },
730+ "methodology": {
731+ "name": "methodology",
732+ "type": "text",
733+ "primaryKey": false,
734+ "notNull": false
735+ },
736+ "evidence_type": {
737+ "name": "evidence_type",
738+ "type": "text",
739+ "primaryKey": false,
740+ "notNull": true
741+ },
742+ "access_level": {
743+ "name": "access_level",
744+ "type": "text",
745+ "primaryKey": false,
746+ "notNull": true,
747+ "default": "'open'"
748+ },
749+ "confidence": {
750+ "name": "confidence",
751+ "type": "real",
752+ "primaryKey": false,
753+ "notNull": false
754+ },
755+ "license": {
756+ "name": "license",
757+ "type": "text",
758+ "primaryKey": false,
759+ "notNull": false
760+ },
761+ "ingest_run_id": {
762+ "name": "ingest_run_id",
763+ "type": "text",
764+ "primaryKey": false,
765+ "notNull": false
766+ },
767+ "created_at": {
768+ "name": "created_at",
769+ "type": "timestamp with time zone",
770+ "primaryKey": false,
771+ "notNull": true,
772+ "default": "now()"
773+ }
774+ },
775+ "indexes": {
776+ "provenance_source_idx": {
777+ "name": "provenance_source_idx",
778+ "columns": [
779+ {
780+ "expression": "source_id",
781+ "isExpression": false,
782+ "asc": true,
783+ "nulls": "last"
784+ },
785+ {
786+ "expression": "source_record_id",
787+ "isExpression": false,
788+ "asc": true,
789+ "nulls": "last"
790+ }
791+ ],
792+ "isUnique": false,
793+ "concurrently": false,
794+ "method": "btree",
795+ "with": {}
796+ },
797+ "provenance_pmid_idx": {
798+ "name": "provenance_pmid_idx",
799+ "columns": [
800+ {
801+ "expression": "pmid",
802+ "isExpression": false,
803+ "asc": true,
804+ "nulls": "last"
805+ }
806+ ],
807+ "isUnique": false,
808+ "concurrently": false,
809+ "method": "btree",
810+ "with": {}
811+ }
812+ },
813+ "foreignKeys": {},
814+ "compositePrimaryKeys": {},
815+ "uniqueConstraints": {},
816+ "policies": {},
817+ "checkConstraints": {},
818+ "isRLSEnabled": false
819+ },
820+ "public.source_records": {
821+ "name": "source_records",
822+ "schema": "",
823+ "columns": {
824+ "id": {
825+ "name": "id",
826+ "type": "bigserial",
827+ "primaryKey": true,
828+ "notNull": true
829+ },
830+ "source_id": {
831+ "name": "source_id",
832+ "type": "varchar(32)",
833+ "primaryKey": false,
834+ "notNull": true
835+ },
836+ "entity_kind": {
837+ "name": "entity_kind",
838+ "type": "text",
839+ "primaryKey": false,
840+ "notNull": true
841+ },
842+ "source_record_id": {
843+ "name": "source_record_id",
844+ "type": "text",
845+ "primaryKey": false,
846+ "notNull": true
847+ },
848+ "payload_hash": {
849+ "name": "payload_hash",
850+ "type": "text",
851+ "primaryKey": false,
852+ "notNull": true
853+ },
854+ "raw_path": {
855+ "name": "raw_path",
856+ "type": "text",
857+ "primaryKey": false,
858+ "notNull": false
859+ },
860+ "status": {
861+ "name": "status",
862+ "type": "text",
863+ "primaryKey": false,
864+ "notNull": true,
865+ "default": "'active'"
866+ },
867+ "first_seen_run": {
868+ "name": "first_seen_run",
869+ "type": "text",
870+ "primaryKey": false,
871+ "notNull": false
872+ },
873+ "last_seen_run": {
874+ "name": "last_seen_run",
875+ "type": "text",
876+ "primaryKey": false,
877+ "notNull": false
878+ },
879+ "retrieved_at": {
880+ "name": "retrieved_at",
881+ "type": "timestamp with time zone",
882+ "primaryKey": false,
883+ "notNull": true,
884+ "default": "now()"
885+ },
886+ "source_updated_at": {
887+ "name": "source_updated_at",
888+ "type": "timestamp with time zone",
889+ "primaryKey": false,
890+ "notNull": false
891+ },
892+ "canonical_type": {
893+ "name": "canonical_type",
894+ "type": "text",
895+ "primaryKey": false,
896+ "notNull": false
897+ },
898+ "canonical_id": {
899+ "name": "canonical_id",
900+ "type": "text",
901+ "primaryKey": false,
902+ "notNull": false
903+ },
904+ "created_at": {
905+ "name": "created_at",
906+ "type": "timestamp with time zone",
907+ "primaryKey": false,
908+ "notNull": true,
909+ "default": "now()"
910+ },
911+ "updated_at": {
912+ "name": "updated_at",
913+ "type": "timestamp with time zone",
914+ "primaryKey": false,
915+ "notNull": true,
916+ "default": "now()"
917+ }
918+ },
919+ "indexes": {
920+ "source_records_uq": {
921+ "name": "source_records_uq",
922+ "columns": [
923+ {
924+ "expression": "source_id",
925+ "isExpression": false,
926+ "asc": true,
927+ "nulls": "last"
928+ },
929+ {
930+ "expression": "entity_kind",
931+ "isExpression": false,
932+ "asc": true,
933+ "nulls": "last"
934+ },
935+ {
936+ "expression": "source_record_id",
937+ "isExpression": false,
938+ "asc": true,
939+ "nulls": "last"
940+ }
941+ ],
942+ "isUnique": true,
943+ "concurrently": false,
944+ "method": "btree",
945+ "with": {}
946+ },
947+ "source_records_canonical_idx": {
948+ "name": "source_records_canonical_idx",
949+ "columns": [
950+ {
951+ "expression": "canonical_type",
952+ "isExpression": false,
953+ "asc": true,
954+ "nulls": "last"
955+ },
956+ {
957+ "expression": "canonical_id",
958+ "isExpression": false,
959+ "asc": true,
960+ "nulls": "last"
961+ }
962+ ],
963+ "isUnique": false,
964+ "concurrently": false,
965+ "method": "btree",
966+ "with": {}
967+ }
968+ },
969+ "foreignKeys": {},
970+ "compositePrimaryKeys": {},
971+ "uniqueConstraints": {},
972+ "policies": {},
973+ "checkConstraints": {},
974+ "isRLSEnabled": false
975+ },
976+ "public.sources": {
977+ "name": "sources",
978+ "schema": "",
979+ "columns": {
980+ "id": {
981+ "name": "id",
982+ "type": "varchar(32)",
983+ "primaryKey": true,
984+ "notNull": true
985+ },
986+ "slug": {
987+ "name": "slug",
988+ "type": "text",
989+ "primaryKey": false,
990+ "notNull": true
991+ },
992+ "name": {
993+ "name": "name",
994+ "type": "text",
995+ "primaryKey": false,
996+ "notNull": true
997+ },
998+ "organization": {
999+ "name": "organization",
1000+ "type": "text",
1001+ "primaryKey": false,
1002+ "notNull": false
1003+ },
1004+ "category": {
1005+ "name": "category",
1006+ "type": "text",
1007+ "primaryKey": false,
1008+ "notNull": true
1009+ },
1010+ "description": {
1011+ "name": "description",
1012+ "type": "text",
1013+ "primaryKey": false,
1014+ "notNull": false
1015+ },
1016+ "homepage": {
1017+ "name": "homepage",
1018+ "type": "text",
1019+ "primaryKey": false,
1020+ "notNull": false
1021+ },
1022+ "docs_url": {
1023+ "name": "docs_url",
1024+ "type": "text",
1025+ "primaryKey": false,
1026+ "notNull": false
1027+ },
1028+ "terms_url": {
1029+ "name": "terms_url",
1030+ "type": "text",
1031+ "primaryKey": false,
1032+ "notNull": false
1033+ },
1034+ "access_type": {
1035+ "name": "access_type",
1036+ "type": "text",
1037+ "primaryKey": false,
1038+ "notNull": true
1039+ },
1040+ "access_auth": {
1041+ "name": "access_auth",
1042+ "type": "text",
1043+ "primaryKey": false,
1044+ "notNull": true
1045+ },
1046+ "license": {
1047+ "name": "license",
1048+ "type": "text",
1049+ "primaryKey": false,
1050+ "notNull": false
1051+ },
1052+ "license_status": {
1053+ "name": "license_status",
1054+ "type": "text",
1055+ "primaryKey": false,
1056+ "notNull": true,
1057+ "default": "'review'"
1058+ },
1059+ "commercial_use": {
1060+ "name": "commercial_use",
1061+ "type": "text",
1062+ "primaryKey": false,
1063+ "notNull": true,
1064+ "default": "'unknown'"
1065+ },
1066+ "redistribution": {
1067+ "name": "redistribution",
1068+ "type": "text",
1069+ "primaryKey": false,
1070+ "notNull": true,
1071+ "default": "'unknown'"
1072+ },
1073+ "attribution": {
1074+ "name": "attribution",
1075+ "type": "text",
1076+ "primaryKey": false,
1077+ "notNull": false
1078+ },
1079+ "license_reviewed_at": {
1080+ "name": "license_reviewed_at",
1081+ "type": "timestamp with time zone",
1082+ "primaryKey": false,
1083+ "notNull": false
1084+ },
1085+ "approved_for_production": {
1086+ "name": "approved_for_production",
1087+ "type": "boolean",
1088+ "primaryKey": false,
1089+ "notNull": true,
1090+ "default": false
1091+ },
1092+ "update_frequency": {
1093+ "name": "update_frequency",
1094+ "type": "text",
1095+ "primaryKey": false,
1096+ "notNull": false
1097+ },
1098+ "supports_incremental": {
1099+ "name": "supports_incremental",
1100+ "type": "boolean",
1101+ "primaryKey": false,
1102+ "notNull": true,
1103+ "default": false
1104+ },
1105+ "entities": {
1106+ "name": "entities",
1107+ "type": "text[]",
1108+ "primaryKey": false,
1109+ "notNull": true,
1110+ "default": "'{}'"
1111+ },
1112+ "metrics": {
1113+ "name": "metrics",
1114+ "type": "text[]",
1115+ "primaryKey": false,
1116+ "notNull": true,
1117+ "default": "'{}'"
1118+ },
1119+ "rate_limit": {
1120+ "name": "rate_limit",
1121+ "type": "text",
1122+ "primaryKey": false,
1123+ "notNull": false
1124+ },
1125+ "status": {
1126+ "name": "status",
1127+ "type": "text",
1128+ "primaryKey": false,
1129+ "notNull": true,
1130+ "default": "'planned'"
1131+ },
1132+ "tier": {
1133+ "name": "tier",
1134+ "type": "integer",
1135+ "primaryKey": false,
1136+ "notNull": true,
1137+ "default": 0
1138+ },
1139+ "manifest": {
1140+ "name": "manifest",
1141+ "type": "jsonb",
1142+ "primaryKey": false,
1143+ "notNull": true,
1144+ "default": "'{}'::jsonb"
1145+ },
1146+ "created_at": {
1147+ "name": "created_at",
1148+ "type": "timestamp with time zone",
1149+ "primaryKey": false,
1150+ "notNull": true,
1151+ "default": "now()"
1152+ },
1153+ "updated_at": {
1154+ "name": "updated_at",
1155+ "type": "timestamp with time zone",
1156+ "primaryKey": false,
1157+ "notNull": true,
1158+ "default": "now()"
1159+ }
1160+ },
1161+ "indexes": {
1162+ "sources_slug_uq": {
1163+ "name": "sources_slug_uq",
1164+ "columns": [
1165+ {
1166+ "expression": "slug",
1167+ "isExpression": false,
1168+ "asc": true,
1169+ "nulls": "last"
1170+ }
1171+ ],
1172+ "isUnique": true,
1173+ "concurrently": false,
1174+ "method": "btree",
1175+ "with": {}
1176+ }
1177+ },
1178+ "foreignKeys": {},
1179+ "compositePrimaryKeys": {},
1180+ "uniqueConstraints": {},
1181+ "policies": {},
1182+ "checkConstraints": {},
1183+ "isRLSEnabled": false
1184+ },
1185+ "public.unresolved_labels": {
1186+ "name": "unresolved_labels",
1187+ "schema": "",
1188+ "columns": {
1189+ "id": {
1190+ "name": "id",
1191+ "type": "bigserial",
1192+ "primaryKey": true,
1193+ "notNull": true
1194+ },
1195+ "source_id": {
1196+ "name": "source_id",
1197+ "type": "varchar(32)",
1198+ "primaryKey": false,
1199+ "notNull": true
1200+ },
1201+ "entity_kind": {
1202+ "name": "entity_kind",
1203+ "type": "text",
1204+ "primaryKey": false,
1205+ "notNull": true
1206+ },
1207+ "source_text": {
1208+ "name": "source_text",
1209+ "type": "text",
1210+ "primaryKey": false,
1211+ "notNull": true
1212+ },
1213+ "normalized": {
1214+ "name": "normalized",
1215+ "type": "text",
1216+ "primaryKey": false,
1217+ "notNull": true
1218+ },
1219+ "context": {
1220+ "name": "context",
1221+ "type": "jsonb",
1222+ "primaryKey": false,
1223+ "notNull": true,
1224+ "default": "'{}'::jsonb"
1225+ },
1226+ "count": {
1227+ "name": "count",
1228+ "type": "integer",
1229+ "primaryKey": false,
1230+ "notNull": true,
1231+ "default": 1
1232+ },
1233+ "status": {
1234+ "name": "status",
1235+ "type": "text",
1236+ "primaryKey": false,
1237+ "notNull": true,
1238+ "default": "'open'"
1239+ },
1240+ "suggested_id": {
1241+ "name": "suggested_id",
1242+ "type": "text",
1243+ "primaryKey": false,
1244+ "notNull": false
1245+ },
1246+ "suggested_match_type": {
1247+ "name": "suggested_match_type",
1248+ "type": "text",
1249+ "primaryKey": false,
1250+ "notNull": false
1251+ },
1252+ "suggested_score": {
1253+ "name": "suggested_score",
1254+ "type": "real",
1255+ "primaryKey": false,
1256+ "notNull": false
1257+ },
1258+ "resolved_id": {
1259+ "name": "resolved_id",
1260+ "type": "text",
1261+ "primaryKey": false,
1262+ "notNull": false
1263+ },
1264+ "resolved_by": {
1265+ "name": "resolved_by",
1266+ "type": "text",
1267+ "primaryKey": false,
1268+ "notNull": false
1269+ },
1270+ "created_at": {
1271+ "name": "created_at",
1272+ "type": "timestamp with time zone",
1273+ "primaryKey": false,
1274+ "notNull": true,
1275+ "default": "now()"
1276+ },
1277+ "updated_at": {
1278+ "name": "updated_at",
1279+ "type": "timestamp with time zone",
1280+ "primaryKey": false,
1281+ "notNull": true,
1282+ "default": "now()"
1283+ }
1284+ },
1285+ "indexes": {
1286+ "unresolved_labels_uq": {
1287+ "name": "unresolved_labels_uq",
1288+ "columns": [
1289+ {
1290+ "expression": "source_id",
1291+ "isExpression": false,
1292+ "asc": true,
1293+ "nulls": "last"
1294+ },
1295+ {
1296+ "expression": "entity_kind",
1297+ "isExpression": false,
1298+ "asc": true,
1299+ "nulls": "last"
1300+ },
1301+ {
1302+ "expression": "normalized",
1303+ "isExpression": false,
1304+ "asc": true,
1305+ "nulls": "last"
1306+ }
1307+ ],
1308+ "isUnique": true,
1309+ "concurrently": false,
1310+ "method": "btree",
1311+ "with": {}
1312+ },
1313+ "unresolved_labels_count_idx": {
1314+ "name": "unresolved_labels_count_idx",
1315+ "columns": [
1316+ {
1317+ "expression": "status",
1318+ "isExpression": false,
1319+ "asc": true,
1320+ "nulls": "last"
1321+ },
1322+ {
1323+ "expression": "count",
1324+ "isExpression": false,
1325+ "asc": true,
1326+ "nulls": "last"
1327+ }
1328+ ],
1329+ "isUnique": false,
1330+ "concurrently": false,
1331+ "method": "btree",
1332+ "with": {}
1333+ }
1334+ },
1335+ "foreignKeys": {},
1336+ "compositePrimaryKeys": {},
1337+ "uniqueConstraints": {},
1338+ "policies": {},
1339+ "checkConstraints": {},
1340+ "isRLSEnabled": false
1341+ },
1342+ "public.anatomical_sites": {
1343+ "name": "anatomical_sites",
1344+ "schema": "",
1345+ "columns": {
1346+ "id": {
1347+ "name": "id",
1348+ "type": "varchar(32)",
1349+ "primaryKey": true,
1350+ "notNull": true
1351+ },
1352+ "name": {
1353+ "name": "name",
1354+ "type": "text",
1355+ "primaryKey": false,
1356+ "notNull": true
1357+ },
1358+ "slug": {
1359+ "name": "slug",
1360+ "type": "text",
1361+ "primaryKey": false,
1362+ "notNull": true
1363+ },
1364+ "ncit_code": {
1365+ "name": "ncit_code",
1366+ "type": "text",
1367+ "primaryKey": false,
1368+ "notNull": false
1369+ },
1370+ "uberon_id": {
1371+ "name": "uberon_id",
1372+ "type": "text",
1373+ "primaryKey": false,
1374+ "notNull": false
1375+ },
1376+ "parent_id": {
1377+ "name": "parent_id",
1378+ "type": "varchar(32)",
1379+ "primaryKey": false,
1380+ "notNull": false
1381+ },
1382+ "system": {
1383+ "name": "system",
1384+ "type": "text",
1385+ "primaryKey": false,
1386+ "notNull": false
1387+ }
1388+ },
1389+ "indexes": {
1390+ "anatomical_sites_slug_uq": {
1391+ "name": "anatomical_sites_slug_uq",
1392+ "columns": [
1393+ {
1394+ "expression": "slug",
1395+ "isExpression": false,
1396+ "asc": true,
1397+ "nulls": "last"
1398+ }
1399+ ],
1400+ "isUnique": true,
1401+ "concurrently": false,
1402+ "method": "btree",
1403+ "with": {}
1404+ }
1405+ },
1406+ "foreignKeys": {},
1407+ "compositePrimaryKeys": {},
1408+ "uniqueConstraints": {},
1409+ "policies": {},
1410+ "checkConstraints": {},
1411+ "isRLSEnabled": false
1412+ },
1413+ "public.cancer_aliases": {
1414+ "name": "cancer_aliases",
1415+ "schema": "",
1416+ "columns": {
1417+ "id": {
1418+ "name": "id",
1419+ "type": "bigserial",
1420+ "primaryKey": true,
1421+ "notNull": true
1422+ },
1423+ "cancer_id": {
1424+ "name": "cancer_id",
1425+ "type": "varchar(32)",
1426+ "primaryKey": false,
1427+ "notNull": true
1428+ },
1429+ "alias": {
1430+ "name": "alias",
1431+ "type": "text",
1432+ "primaryKey": false,
1433+ "notNull": true
1434+ },
1435+ "normalized": {
1436+ "name": "normalized",
1437+ "type": "text",
1438+ "primaryKey": false,
1439+ "notNull": true
1440+ },
1441+ "alias_type": {
1442+ "name": "alias_type",
1443+ "type": "text",
1444+ "primaryKey": false,
1445+ "notNull": true,
1446+ "default": "'synonym'"
1447+ },
1448+ "source_id": {
1449+ "name": "source_id",
1450+ "type": "varchar(32)",
1451+ "primaryKey": false,
1452+ "notNull": false
1453+ },
1454+ "source_terminology": {
1455+ "name": "source_terminology",
1456+ "type": "text",
1457+ "primaryKey": false,
1458+ "notNull": false
1459+ },
1460+ "language": {
1461+ "name": "language",
1462+ "type": "text",
1463+ "primaryKey": false,
1464+ "notNull": true,
1465+ "default": "'en'"
1466+ }
1467+ },
1468+ "indexes": {
1469+ "cancer_aliases_uq": {
1470+ "name": "cancer_aliases_uq",
1471+ "columns": [
1472+ {
1473+ "expression": "cancer_id",
1474+ "isExpression": false,
1475+ "asc": true,
1476+ "nulls": "last"
1477+ },
1478+ {
1479+ "expression": "normalized",
1480+ "isExpression": false,
1481+ "asc": true,
1482+ "nulls": "last"
1483+ },
1484+ {
1485+ "expression": "alias_type",
1486+ "isExpression": false,
1487+ "asc": true,
1488+ "nulls": "last"
1489+ }
1490+ ],
1491+ "isUnique": true,
1492+ "concurrently": false,
1493+ "method": "btree",
1494+ "with": {}
1495+ },
1496+ "cancer_aliases_norm_idx": {
1497+ "name": "cancer_aliases_norm_idx",
1498+ "columns": [
1499+ {
1500+ "expression": "normalized",
1501+ "isExpression": false,
1502+ "asc": true,
1503+ "nulls": "last"
1504+ }
1505+ ],
1506+ "isUnique": false,
1507+ "concurrently": false,
1508+ "method": "btree",
1509+ "with": {}
1510+ }
1511+ },
1512+ "foreignKeys": {},
1513+ "compositePrimaryKeys": {},
1514+ "uniqueConstraints": {},
1515+ "policies": {},
1516+ "checkConstraints": {},
1517+ "isRLSEnabled": false
1518+ },
1519+ "public.cancer_anatomy": {
1520+ "name": "cancer_anatomy",
1521+ "schema": "",
1522+ "columns": {
1523+ "id": {
1524+ "name": "id",
1525+ "type": "bigserial",
1526+ "primaryKey": true,
1527+ "notNull": true
1528+ },
1529+ "cancer_id": {
1530+ "name": "cancer_id",
1531+ "type": "varchar(32)",
1532+ "primaryKey": false,
1533+ "notNull": true
1534+ },
1535+ "site_id": {
1536+ "name": "site_id",
1537+ "type": "varchar(32)",
1538+ "primaryKey": false,
1539+ "notNull": true
1540+ },
1541+ "relation": {
1542+ "name": "relation",
1543+ "type": "text",
1544+ "primaryKey": false,
1545+ "notNull": true,
1546+ "default": "'primary'"
1547+ },
1548+ "source_id": {
1549+ "name": "source_id",
1550+ "type": "varchar(32)",
1551+ "primaryKey": false,
1552+ "notNull": false
1553+ }
1554+ },
1555+ "indexes": {
1556+ "cancer_anatomy_uq": {
1557+ "name": "cancer_anatomy_uq",
1558+ "columns": [
1559+ {
1560+ "expression": "cancer_id",
1561+ "isExpression": false,
1562+ "asc": true,
1563+ "nulls": "last"
1564+ },
1565+ {
1566+ "expression": "site_id",
1567+ "isExpression": false,
1568+ "asc": true,
1569+ "nulls": "last"
1570+ },
1571+ {
1572+ "expression": "relation",
1573+ "isExpression": false,
1574+ "asc": true,
1575+ "nulls": "last"
1576+ }
1577+ ],
1578+ "isUnique": true,
1579+ "concurrently": false,
1580+ "method": "btree",
1581+ "with": {}
1582+ }
1583+ },
1584+ "foreignKeys": {},
1585+ "compositePrimaryKeys": {},
1586+ "uniqueConstraints": {},
1587+ "policies": {},
1588+ "checkConstraints": {},
1589+ "isRLSEnabled": false
1590+ },
1591+ "public.cancer_codes": {
1592+ "name": "cancer_codes",
1593+ "schema": "",
1594+ "columns": {
1595+ "id": {
1596+ "name": "id",
1597+ "type": "bigserial",
1598+ "primaryKey": true,
1599+ "notNull": true
1600+ },
1601+ "cancer_id": {
1602+ "name": "cancer_id",
1603+ "type": "varchar(32)",
1604+ "primaryKey": false,
1605+ "notNull": true
1606+ },
1607+ "system": {
1608+ "name": "system",
1609+ "type": "text",
1610+ "primaryKey": false,
1611+ "notNull": true
1612+ },
1613+ "code": {
1614+ "name": "code",
1615+ "type": "text",
1616+ "primaryKey": false,
1617+ "notNull": true
1618+ },
1619+ "match_type": {
1620+ "name": "match_type",
1621+ "type": "text",
1622+ "primaryKey": false,
1623+ "notNull": true,
1624+ "default": "'EXACT_IDENTIFIER'"
1625+ },
1626+ "source_id": {
1627+ "name": "source_id",
1628+ "type": "varchar(32)",
1629+ "primaryKey": false,
1630+ "notNull": false
1631+ },
1632+ "valid_from": {
1633+ "name": "valid_from",
1634+ "type": "text",
1635+ "primaryKey": false,
1636+ "notNull": false
1637+ },
1638+ "valid_to": {
1639+ "name": "valid_to",
1640+ "type": "text",
1641+ "primaryKey": false,
1642+ "notNull": false
1643+ }
1644+ },
1645+ "indexes": {
1646+ "cancer_codes_uq": {
1647+ "name": "cancer_codes_uq",
1648+ "columns": [
1649+ {
1650+ "expression": "cancer_id",
1651+ "isExpression": false,
1652+ "asc": true,
1653+ "nulls": "last"
1654+ },
1655+ {
1656+ "expression": "system",
1657+ "isExpression": false,
1658+ "asc": true,
1659+ "nulls": "last"
1660+ },
1661+ {
1662+ "expression": "code",
1663+ "isExpression": false,
1664+ "asc": true,
1665+ "nulls": "last"
1666+ }
1667+ ],
1668+ "isUnique": true,
1669+ "concurrently": false,
1670+ "method": "btree",
1671+ "with": {}
1672+ },
1673+ "cancer_codes_lookup_idx": {
1674+ "name": "cancer_codes_lookup_idx",
1675+ "columns": [
1676+ {
1677+ "expression": "system",
1678+ "isExpression": false,
1679+ "asc": true,
1680+ "nulls": "last"
1681+ },
1682+ {
1683+ "expression": "code",
1684+ "isExpression": false,
1685+ "asc": true,
1686+ "nulls": "last"
1687+ }
1688+ ],
1689+ "isUnique": false,
1690+ "concurrently": false,
1691+ "method": "btree",
1692+ "with": {}
1693+ }
1694+ },
1695+ "foreignKeys": {},
1696+ "compositePrimaryKeys": {},
1697+ "uniqueConstraints": {},
1698+ "policies": {},
1699+ "checkConstraints": {},
1700+ "isRLSEnabled": false
1701+ },
1702+ "public.cancer_hierarchy": {
1703+ "name": "cancer_hierarchy",
1704+ "schema": "",
1705+ "columns": {
1706+ "id": {
1707+ "name": "id",
1708+ "type": "bigserial",
1709+ "primaryKey": true,
1710+ "notNull": true
1711+ },
1712+ "parent_id": {
1713+ "name": "parent_id",
1714+ "type": "varchar(32)",
1715+ "primaryKey": false,
1716+ "notNull": true
1717+ },
1718+ "child_id": {
1719+ "name": "child_id",
1720+ "type": "varchar(32)",
1721+ "primaryKey": false,
1722+ "notNull": true
1723+ },
1724+ "hierarchy_type": {
1725+ "name": "hierarchy_type",
1726+ "type": "text",
1727+ "primaryKey": false,
1728+ "notNull": true
1729+ },
1730+ "source_id": {
1731+ "name": "source_id",
1732+ "type": "varchar(32)",
1733+ "primaryKey": false,
1734+ "notNull": false
1735+ }
1736+ },
1737+ "indexes": {
1738+ "cancer_hierarchy_uq": {
1739+ "name": "cancer_hierarchy_uq",
1740+ "columns": [
1741+ {
1742+ "expression": "parent_id",
1743+ "isExpression": false,
1744+ "asc": true,
1745+ "nulls": "last"
1746+ },
1747+ {
1748+ "expression": "child_id",
1749+ "isExpression": false,
1750+ "asc": true,
1751+ "nulls": "last"
1752+ },
1753+ {
1754+ "expression": "hierarchy_type",
1755+ "isExpression": false,
1756+ "asc": true,
1757+ "nulls": "last"
1758+ }
1759+ ],
1760+ "isUnique": true,
1761+ "concurrently": false,
1762+ "method": "btree",
1763+ "with": {}
1764+ },
1765+ "cancer_hierarchy_child_idx": {
1766+ "name": "cancer_hierarchy_child_idx",
1767+ "columns": [
1768+ {
1769+ "expression": "child_id",
1770+ "isExpression": false,
1771+ "asc": true,
1772+ "nulls": "last"
1773+ }
1774+ ],
1775+ "isUnique": false,
1776+ "concurrently": false,
1777+ "method": "btree",
1778+ "with": {}
1779+ }
1780+ },
1781+ "foreignKeys": {},
1782+ "compositePrimaryKeys": {},
1783+ "uniqueConstraints": {},
1784+ "policies": {},
1785+ "checkConstraints": {},
1786+ "isRLSEnabled": false
1787+ },
1788+ "public.cancers": {
1789+ "name": "cancers",
1790+ "schema": "",
1791+ "columns": {
1792+ "id": {
1793+ "name": "id",
1794+ "type": "varchar(32)",
1795+ "primaryKey": true,
1796+ "notNull": true
1797+ },
1798+ "slug": {
1799+ "name": "slug",
1800+ "type": "text",
1801+ "primaryKey": false,
1802+ "notNull": true
1803+ },
1804+ "canonical_name": {
1805+ "name": "canonical_name",
1806+ "type": "text",
1807+ "primaryKey": false,
1808+ "notNull": true
1809+ },
1810+ "short_name": {
1811+ "name": "short_name",
1812+ "type": "text",
1813+ "primaryKey": false,
1814+ "notNull": false
1815+ },
1816+ "entity_type": {
1817+ "name": "entity_type",
1818+ "type": "text",
1819+ "primaryKey": false,
1820+ "notNull": true,
1821+ "default": "'cancer'"
1822+ },
1823+ "malignant": {
1824+ "name": "malignant",
1825+ "type": "boolean",
1826+ "primaryKey": false,
1827+ "notNull": true,
1828+ "default": true
1829+ },
1830+ "solid_tumor": {
1831+ "name": "solid_tumor",
1832+ "type": "boolean",
1833+ "primaryKey": false,
1834+ "notNull": true,
1835+ "default": true
1836+ },
1837+ "hematologic": {
1838+ "name": "hematologic",
1839+ "type": "boolean",
1840+ "primaryKey": false,
1841+ "notNull": true,
1842+ "default": false
1843+ },
1844+ "pediatric_relevant": {
1845+ "name": "pediatric_relevant",
1846+ "type": "boolean",
1847+ "primaryKey": false,
1848+ "notNull": true,
1849+ "default": false
1850+ },
1851+ "rare_cancer": {
1852+ "name": "rare_cancer",
1853+ "type": "boolean",
1854+ "primaryKey": false,
1855+ "notNull": false
1856+ },
1857+ "top_level": {
1858+ "name": "top_level",
1859+ "type": "boolean",
1860+ "primaryKey": false,
1861+ "notNull": true,
1862+ "default": false
1863+ },
1864+ "description": {
1865+ "name": "description",
1866+ "type": "text",
1867+ "primaryKey": false,
1868+ "notNull": false
1869+ },
1870+ "description_provenance_id": {
1871+ "name": "description_provenance_id",
1872+ "type": "integer",
1873+ "primaryKey": false,
1874+ "notNull": false
1875+ },
1876+ "primary_ncit_code": {
1877+ "name": "primary_ncit_code",
1878+ "type": "text",
1879+ "primaryKey": false,
1880+ "notNull": false
1881+ },
1882+ "primary_oncotree_code": {
1883+ "name": "primary_oncotree_code",
1884+ "type": "text",
1885+ "primaryKey": false,
1886+ "notNull": false
1887+ },
1888+ "depth": {
1889+ "name": "depth",
1890+ "type": "integer",
1891+ "primaryKey": false,
1892+ "notNull": true,
1893+ "default": 0
1894+ },
1895+ "status": {
1896+ "name": "status",
1897+ "type": "text",
1898+ "primaryKey": false,
1899+ "notNull": true,
1900+ "default": "'active'"
1901+ },
1902+ "merged_into": {
1903+ "name": "merged_into",
1904+ "type": "varchar(32)",
1905+ "primaryKey": false,
1906+ "notNull": false
1907+ },
1908+ "deprecated_reason": {
1909+ "name": "deprecated_reason",
1910+ "type": "text",
1911+ "primaryKey": false,
1912+ "notNull": false
1913+ },
1914+ "classification_version": {
1915+ "name": "classification_version",
1916+ "type": "text",
1917+ "primaryKey": false,
1918+ "notNull": false
1919+ },
1920+ "semantic_types": {
1921+ "name": "semantic_types",
1922+ "type": "text[]",
1923+ "primaryKey": false,
1924+ "notNull": true,
1925+ "default": "'{}'"
1926+ },
1927+ "created_at": {
1928+ "name": "created_at",
1929+ "type": "timestamp with time zone",
1930+ "primaryKey": false,
1931+ "notNull": true,
1932+ "default": "now()"
1933+ },
1934+ "updated_at": {
1935+ "name": "updated_at",
1936+ "type": "timestamp with time zone",
1937+ "primaryKey": false,
1938+ "notNull": true,
1939+ "default": "now()"
1940+ }
1941+ },
1942+ "indexes": {
1943+ "cancers_slug_uq": {
1944+ "name": "cancers_slug_uq",
1945+ "columns": [
1946+ {
1947+ "expression": "slug",
1948+ "isExpression": false,
1949+ "asc": true,
1950+ "nulls": "last"
1951+ }
1952+ ],
1953+ "isUnique": true,
1954+ "concurrently": false,
1955+ "method": "btree",
1956+ "with": {}
1957+ },
1958+ "cancers_ncit_uq": {
1959+ "name": "cancers_ncit_uq",
1960+ "columns": [
1961+ {
1962+ "expression": "primary_ncit_code",
1963+ "isExpression": false,
1964+ "asc": true,
1965+ "nulls": "last"
1966+ }
1967+ ],
1968+ "isUnique": true,
1969+ "concurrently": false,
1970+ "method": "btree",
1971+ "with": {}
1972+ },
1973+ "cancers_name_idx": {
1974+ "name": "cancers_name_idx",
1975+ "columns": [
1976+ {
1977+ "expression": "canonical_name",
1978+ "isExpression": false,
1979+ "asc": true,
1980+ "nulls": "last"
1981+ }
1982+ ],
1983+ "isUnique": false,
1984+ "concurrently": false,
1985+ "method": "btree",
1986+ "with": {}
1987+ },
1988+ "cancers_type_idx": {
1989+ "name": "cancers_type_idx",
1990+ "columns": [
1991+ {
1992+ "expression": "entity_type",
1993+ "isExpression": false,
1994+ "asc": true,
1995+ "nulls": "last"
1996+ },
1997+ {
1998+ "expression": "malignant",
1999+ "isExpression": false,
2000+ "asc": true,
2001+ "nulls": "last"
2002+ },
2003+ {
2004+ "expression": "top_level",
2005+ "isExpression": false,
2006+ "asc": true,
2007+ "nulls": "last"
2008+ }
2009+ ],
2010+ "isUnique": false,
2011+ "concurrently": false,
2012+ "method": "btree",
2013+ "with": {}
2014+ }
2015+ },
2016+ "foreignKeys": {},
2017+ "compositePrimaryKeys": {},
2018+ "uniqueConstraints": {},
2019+ "policies": {},
2020+ "checkConstraints": {},
2021+ "isRLSEnabled": false
2022+ },
2023+ "public.cohort_definitions": {
2024+ "name": "cohort_definitions",
2025+ "schema": "",
2026+ "columns": {
2027+ "id": {
2028+ "name": "id",
2029+ "type": "bigserial",
2030+ "primaryKey": true,
2031+ "notNull": true
2032+ },
2033+ "name": {
2034+ "name": "name",
2035+ "type": "text",
2036+ "primaryKey": false,
2037+ "notNull": true
2038+ },
2039+ "cancer_id": {
2040+ "name": "cancer_id",
2041+ "type": "varchar(32)",
2042+ "primaryKey": false,
2043+ "notNull": true
2044+ },
2045+ "biomarker_ids": {
2046+ "name": "biomarker_ids",
2047+ "type": "text[]",
2048+ "primaryKey": false,
2049+ "notNull": true,
2050+ "default": "'{}'"
2051+ },
2052+ "variant_ids": {
2053+ "name": "variant_ids",
2054+ "type": "text[]",
2055+ "primaryKey": false,
2056+ "notNull": true,
2057+ "default": "'{}'"
2058+ },
2059+ "stage": {
2060+ "name": "stage",
2061+ "type": "text",
2062+ "primaryKey": false,
2063+ "notNull": false
2064+ },
2065+ "attributes": {
2066+ "name": "attributes",
2067+ "type": "jsonb",
2068+ "primaryKey": false,
2069+ "notNull": true,
2070+ "default": "'{}'::jsonb"
2071+ },
2072+ "confidence": {
2073+ "name": "confidence",
2074+ "type": "real",
2075+ "primaryKey": false,
2076+ "notNull": false
2077+ },
2078+ "created_at": {
2079+ "name": "created_at",
2080+ "type": "timestamp with time zone",
2081+ "primaryKey": false,
2082+ "notNull": true,
2083+ "default": "now()"
2084+ }
2085+ },
2086+ "indexes": {},
2087+ "foreignKeys": {},
2088+ "compositePrimaryKeys": {},
2089+ "uniqueConstraints": {},
2090+ "policies": {},
2091+ "checkConstraints": {},
2092+ "isRLSEnabled": false
2093+ },
2094+ "public.geographies": {
2095+ "name": "geographies",
2096+ "schema": "",
2097+ "columns": {
2098+ "id": {
2099+ "name": "id",
2100+ "type": "varchar(32)",
2101+ "primaryKey": true,
2102+ "notNull": true
2103+ },
2104+ "slug": {
2105+ "name": "slug",
2106+ "type": "text",
2107+ "primaryKey": false,
2108+ "notNull": true
2109+ },
2110+ "name": {
2111+ "name": "name",
2112+ "type": "text",
2113+ "primaryKey": false,
2114+ "notNull": true
2115+ },
2116+ "kind": {
2117+ "name": "kind",
2118+ "type": "text",
2119+ "primaryKey": false,
2120+ "notNull": true
2121+ },
2122+ "iso2": {
2123+ "name": "iso2",
2124+ "type": "text",
2125+ "primaryKey": false,
2126+ "notNull": false
2127+ },
2128+ "iso3": {
2129+ "name": "iso3",
2130+ "type": "text",
2131+ "primaryKey": false,
2132+ "notNull": false
2133+ },
2134+ "parent_id": {
2135+ "name": "parent_id",
2136+ "type": "varchar(32)",
2137+ "primaryKey": false,
2138+ "notNull": false
2139+ },
2140+ "who_region": {
2141+ "name": "who_region",
2142+ "type": "text",
2143+ "primaryKey": false,
2144+ "notNull": false
2145+ },
2146+ "population": {
2147+ "name": "population",
2148+ "type": "integer",
2149+ "primaryKey": false,
2150+ "notNull": false
2151+ },
2152+ "population_year": {
2153+ "name": "population_year",
2154+ "type": "integer",
2155+ "primaryKey": false,
2156+ "notNull": false
2157+ }
2158+ },
2159+ "indexes": {
2160+ "geographies_slug_uq": {
2161+ "name": "geographies_slug_uq",
2162+ "columns": [
2163+ {
2164+ "expression": "slug",
2165+ "isExpression": false,
2166+ "asc": true,
2167+ "nulls": "last"
2168+ }
2169+ ],
2170+ "isUnique": true,
2171+ "concurrently": false,
2172+ "method": "btree",
2173+ "with": {}
2174+ },
2175+ "geographies_iso3_idx": {
2176+ "name": "geographies_iso3_idx",
2177+ "columns": [
2178+ {
2179+ "expression": "iso3",
2180+ "isExpression": false,
2181+ "asc": true,
2182+ "nulls": "last"
2183+ }
2184+ ],
2185+ "isUnique": false,
2186+ "concurrently": false,
2187+ "method": "btree",
2188+ "with": {}
2189+ }
2190+ },
2191+ "foreignKeys": {},
2192+ "compositePrimaryKeys": {},
2193+ "uniqueConstraints": {},
2194+ "policies": {},
2195+ "checkConstraints": {},
2196+ "isRLSEnabled": false
2197+ },
2198+ "public.biomarkers": {
2199+ "name": "biomarkers",
2200+ "schema": "",
2201+ "columns": {
2202+ "id": {
2203+ "name": "id",
2204+ "type": "varchar(32)",
2205+ "primaryKey": true,
2206+ "notNull": true
2207+ },
2208+ "slug": {
2209+ "name": "slug",
2210+ "type": "text",
2211+ "primaryKey": false,
2212+ "notNull": true
2213+ },
2214+ "name": {
2215+ "name": "name",
2216+ "type": "text",
2217+ "primaryKey": false,
2218+ "notNull": true
2219+ },
2220+ "kind": {
2221+ "name": "kind",
2222+ "type": "text",
2223+ "primaryKey": false,
2224+ "notNull": true
2225+ },
2226+ "gene_id": {
2227+ "name": "gene_id",
2228+ "type": "varchar(32)",
2229+ "primaryKey": false,
2230+ "notNull": false
2231+ },
2232+ "ncit_code": {
2233+ "name": "ncit_code",
2234+ "type": "text",
2235+ "primaryKey": false,
2236+ "notNull": false
2237+ },
2238+ "description": {
2239+ "name": "description",
2240+ "type": "text",
2241+ "primaryKey": false,
2242+ "notNull": false
2243+ },
2244+ "measurement": {
2245+ "name": "measurement",
2246+ "type": "jsonb",
2247+ "primaryKey": false,
2248+ "notNull": true,
2249+ "default": "'{}'::jsonb"
2250+ },
2251+ "created_at": {
2252+ "name": "created_at",
2253+ "type": "timestamp with time zone",
2254+ "primaryKey": false,
2255+ "notNull": true,
2256+ "default": "now()"
2257+ },
2258+ "updated_at": {
2259+ "name": "updated_at",
2260+ "type": "timestamp with time zone",
2261+ "primaryKey": false,
2262+ "notNull": true,
2263+ "default": "now()"
2264+ }
2265+ },
2266+ "indexes": {
2267+ "biomarkers_slug_uq": {
2268+ "name": "biomarkers_slug_uq",
2269+ "columns": [
2270+ {
2271+ "expression": "slug",
2272+ "isExpression": false,
2273+ "asc": true,
2274+ "nulls": "last"
2275+ }
2276+ ],
2277+ "isUnique": true,
2278+ "concurrently": false,
2279+ "method": "btree",
2280+ "with": {}
2281+ }
2282+ },
2283+ "foreignKeys": {},
2284+ "compositePrimaryKeys": {},
2285+ "uniqueConstraints": {},
2286+ "policies": {},
2287+ "checkConstraints": {},
2288+ "isRLSEnabled": false
2289+ },
2290+ "public.cancer_gene_frequencies": {
2291+ "name": "cancer_gene_frequencies",
2292+ "schema": "",
2293+ "columns": {
2294+ "id": {
2295+ "name": "id",
2296+ "type": "bigserial",
2297+ "primaryKey": true,
2298+ "notNull": true
2299+ },
2300+ "cohort_id": {
2301+ "name": "cohort_id",
2302+ "type": "varchar(32)",
2303+ "primaryKey": false,
2304+ "notNull": true
2305+ },
2306+ "cancer_id": {
2307+ "name": "cancer_id",
2308+ "type": "varchar(32)",
2309+ "primaryKey": false,
2310+ "notNull": false
2311+ },
2312+ "gene_id": {
2313+ "name": "gene_id",
2314+ "type": "varchar(32)",
2315+ "primaryKey": false,
2316+ "notNull": false
2317+ },
2318+ "gene_symbol": {
2319+ "name": "gene_symbol",
2320+ "type": "text",
2321+ "primaryKey": false,
2322+ "notNull": true
2323+ },
2324+ "alteration_type": {
2325+ "name": "alteration_type",
2326+ "type": "text",
2327+ "primaryKey": false,
2328+ "notNull": true,
2329+ "default": "'ssm'"
2330+ },
2331+ "cases_affected": {
2332+ "name": "cases_affected",
2333+ "type": "integer",
2334+ "primaryKey": false,
2335+ "notNull": true
2336+ },
2337+ "cases_profiled": {
2338+ "name": "cases_profiled",
2339+ "type": "integer",
2340+ "primaryKey": false,
2341+ "notNull": true
2342+ },
2343+ "frequency": {
2344+ "name": "frequency",
2345+ "type": "real",
2346+ "primaryKey": false,
2347+ "notNull": true
2348+ },
2349+ "rank": {
2350+ "name": "rank",
2351+ "type": "integer",
2352+ "primaryKey": false,
2353+ "notNull": false
2354+ },
2355+ "data_release": {
2356+ "name": "data_release",
2357+ "type": "text",
2358+ "primaryKey": false,
2359+ "notNull": false
2360+ },
2361+ "provenance_id": {
2362+ "name": "provenance_id",
2363+ "type": "integer",
2364+ "primaryKey": false,
2365+ "notNull": true
2366+ },
2367+ "updated_at": {
2368+ "name": "updated_at",
2369+ "type": "timestamp with time zone",
2370+ "primaryKey": false,
2371+ "notNull": true,
2372+ "default": "now()"
2373+ }
2374+ },
2375+ "indexes": {
2376+ "cancer_gene_freq_uq": {
2377+ "name": "cancer_gene_freq_uq",
2378+ "columns": [
2379+ {
2380+ "expression": "cohort_id",
2381+ "isExpression": false,
2382+ "asc": true,
2383+ "nulls": "last"
2384+ },
2385+ {
2386+ "expression": "gene_symbol",
2387+ "isExpression": false,
2388+ "asc": true,
2389+ "nulls": "last"
2390+ },
2391+ {
2392+ "expression": "alteration_type",
2393+ "isExpression": false,
2394+ "asc": true,
2395+ "nulls": "last"
2396+ }
2397+ ],
2398+ "isUnique": true,
2399+ "concurrently": false,
2400+ "method": "btree",
2401+ "with": {}
2402+ },
2403+ "cancer_gene_freq_cancer_idx": {
2404+ "name": "cancer_gene_freq_cancer_idx",
2405+ "columns": [
2406+ {
2407+ "expression": "cancer_id",
2408+ "isExpression": false,
2409+ "asc": true,
2410+ "nulls": "last"
2411+ },
2412+ {
2413+ "expression": "frequency",
2414+ "isExpression": false,
2415+ "asc": true,
2416+ "nulls": "last"
2417+ }
2418+ ],
2419+ "isUnique": false,
2420+ "concurrently": false,
2421+ "method": "btree",
2422+ "with": {}
2423+ },
2424+ "cancer_gene_freq_gene_idx": {
2425+ "name": "cancer_gene_freq_gene_idx",
2426+ "columns": [
2427+ {
2428+ "expression": "gene_id",
2429+ "isExpression": false,
2430+ "asc": true,
2431+ "nulls": "last"
2432+ }
2433+ ],
2434+ "isUnique": false,
2435+ "concurrently": false,
2436+ "method": "btree",
2437+ "with": {}
2438+ }
2439+ },
2440+ "foreignKeys": {},
2441+ "compositePrimaryKeys": {},
2442+ "uniqueConstraints": {},
2443+ "policies": {},
2444+ "checkConstraints": {},
2445+ "isRLSEnabled": false
2446+ },
2447+ "public.entity_embeddings": {
2448+ "name": "entity_embeddings",
2449+ "schema": "",
2450+ "columns": {
2451+ "id": {
2452+ "name": "id",
2453+ "type": "bigserial",
2454+ "primaryKey": true,
2455+ "notNull": true
2456+ },
2457+ "entity_type": {
2458+ "name": "entity_type",
2459+ "type": "text",
2460+ "primaryKey": false,
2461+ "notNull": true
2462+ },
2463+ "entity_id": {
2464+ "name": "entity_id",
2465+ "type": "text",
2466+ "primaryKey": false,
2467+ "notNull": true
2468+ },
2469+ "model": {
2470+ "name": "model",
2471+ "type": "text",
2472+ "primaryKey": false,
2473+ "notNull": true
2474+ },
2475+ "dimensions": {
2476+ "name": "dimensions",
2477+ "type": "integer",
2478+ "primaryKey": false,
2479+ "notNull": true
2480+ },
2481+ "text_hash": {
2482+ "name": "text_hash",
2483+ "type": "text",
2484+ "primaryKey": false,
2485+ "notNull": true
2486+ },
2487+ "embedding": {
2488+ "name": "embedding",
2489+ "type": "text",
2490+ "primaryKey": false,
2491+ "notNull": false
2492+ },
2493+ "created_at": {
2494+ "name": "created_at",
2495+ "type": "timestamp with time zone",
2496+ "primaryKey": false,
2497+ "notNull": true,
2498+ "default": "now()"
2499+ }
2500+ },
2501+ "indexes": {
2502+ "entity_embeddings_uq": {
2503+ "name": "entity_embeddings_uq",
2504+ "columns": [
2505+ {
2506+ "expression": "entity_type",
2507+ "isExpression": false,
2508+ "asc": true,
2509+ "nulls": "last"
2510+ },
2511+ {
2512+ "expression": "entity_id",
2513+ "isExpression": false,
2514+ "asc": true,
2515+ "nulls": "last"
2516+ },
2517+ {
2518+ "expression": "model",
2519+ "isExpression": false,
2520+ "asc": true,
2521+ "nulls": "last"
2522+ }
2523+ ],
2524+ "isUnique": true,
2525+ "concurrently": false,
2526+ "method": "btree",
2527+ "with": {}
2528+ }
2529+ },
2530+ "foreignKeys": {},
2531+ "compositePrimaryKeys": {},
2532+ "uniqueConstraints": {},
2533+ "policies": {},
2534+ "checkConstraints": {},
2535+ "isRLSEnabled": false
2536+ },
2537+ "public.gene_aliases": {
2538+ "name": "gene_aliases",
2539+ "schema": "",
2540+ "columns": {
2541+ "id": {
2542+ "name": "id",
2543+ "type": "bigserial",
2544+ "primaryKey": true,
2545+ "notNull": true
2546+ },
2547+ "gene_id": {
2548+ "name": "gene_id",
2549+ "type": "varchar(32)",
2550+ "primaryKey": false,
2551+ "notNull": true
2552+ },
2553+ "alias": {
2554+ "name": "alias",
2555+ "type": "text",
2556+ "primaryKey": false,
2557+ "notNull": true
2558+ },
2559+ "alias_type": {
2560+ "name": "alias_type",
2561+ "type": "text",
2562+ "primaryKey": false,
2563+ "notNull": true
2564+ },
2565+ "source_id": {
2566+ "name": "source_id",
2567+ "type": "varchar(32)",
2568+ "primaryKey": false,
2569+ "notNull": false
2570+ }
2571+ },
2572+ "indexes": {
2573+ "gene_aliases_uq": {
2574+ "name": "gene_aliases_uq",
2575+ "columns": [
2576+ {
2577+ "expression": "gene_id",
2578+ "isExpression": false,
2579+ "asc": true,
2580+ "nulls": "last"
2581+ },
2582+ {
2583+ "expression": "alias",
2584+ "isExpression": false,
2585+ "asc": true,
2586+ "nulls": "last"
2587+ },
2588+ {
2589+ "expression": "alias_type",
2590+ "isExpression": false,
2591+ "asc": true,
2592+ "nulls": "last"
2593+ }
2594+ ],
2595+ "isUnique": true,
2596+ "concurrently": false,
2597+ "method": "btree",
2598+ "with": {}
2599+ },
2600+ "gene_aliases_alias_idx": {
2601+ "name": "gene_aliases_alias_idx",
2602+ "columns": [
2603+ {
2604+ "expression": "alias",
2605+ "isExpression": false,
2606+ "asc": true,
2607+ "nulls": "last"
2608+ }
2609+ ],
2610+ "isUnique": false,
2611+ "concurrently": false,
2612+ "method": "btree",
2613+ "with": {}
2614+ }
2615+ },
2616+ "foreignKeys": {},
2617+ "compositePrimaryKeys": {},
2618+ "uniqueConstraints": {},
2619+ "policies": {},
2620+ "checkConstraints": {},
2621+ "isRLSEnabled": false
2622+ },
2623+ "public.genes": {
2624+ "name": "genes",
2625+ "schema": "",
2626+ "columns": {
2627+ "id": {
2628+ "name": "id",
2629+ "type": "varchar(32)",
2630+ "primaryKey": true,
2631+ "notNull": true
2632+ },
2633+ "hgnc_id": {
2634+ "name": "hgnc_id",
2635+ "type": "text",
2636+ "primaryKey": false,
2637+ "notNull": false
2638+ },
2639+ "symbol": {
2640+ "name": "symbol",
2641+ "type": "text",
2642+ "primaryKey": false,
2643+ "notNull": true
2644+ },
2645+ "name": {
2646+ "name": "name",
2647+ "type": "text",
2648+ "primaryKey": false,
2649+ "notNull": false
2650+ },
2651+ "locus_type": {
2652+ "name": "locus_type",
2653+ "type": "text",
2654+ "primaryKey": false,
2655+ "notNull": false
2656+ },
2657+ "locus_group": {
2658+ "name": "locus_group",
2659+ "type": "text",
2660+ "primaryKey": false,
2661+ "notNull": false
2662+ },
2663+ "location": {
2664+ "name": "location",
2665+ "type": "text",
2666+ "primaryKey": false,
2667+ "notNull": false
2668+ },
2669+ "chromosome": {
2670+ "name": "chromosome",
2671+ "type": "text",
2672+ "primaryKey": false,
2673+ "notNull": false
2674+ },
2675+ "ensembl_gene_id": {
2676+ "name": "ensembl_gene_id",
2677+ "type": "text",
2678+ "primaryKey": false,
2679+ "notNull": false
2680+ },
2681+ "ncbi_gene_id": {
2682+ "name": "ncbi_gene_id",
2683+ "type": "text",
2684+ "primaryKey": false,
2685+ "notNull": false
2686+ },
2687+ "omim_ids": {
2688+ "name": "omim_ids",
2689+ "type": "text[]",
2690+ "primaryKey": false,
2691+ "notNull": true,
2692+ "default": "'{}'"
2693+ },
2694+ "uniprot_ids": {
2695+ "name": "uniprot_ids",
2696+ "type": "text[]",
2697+ "primaryKey": false,
2698+ "notNull": true,
2699+ "default": "'{}'"
2700+ },
2701+ "refseq_accession": {
2702+ "name": "refseq_accession",
2703+ "type": "text",
2704+ "primaryKey": false,
2705+ "notNull": false
2706+ },
2707+ "prev_symbols": {
2708+ "name": "prev_symbols",
2709+ "type": "text[]",
2710+ "primaryKey": false,
2711+ "notNull": true,
2712+ "default": "'{}'"
2713+ },
2714+ "alias_symbols": {
2715+ "name": "alias_symbols",
2716+ "type": "text[]",
2717+ "primaryKey": false,
2718+ "notNull": true,
2719+ "default": "'{}'"
2720+ },
2721+ "gene_families": {
2722+ "name": "gene_families",
2723+ "type": "text[]",
2724+ "primaryKey": false,
2725+ "notNull": true,
2726+ "default": "'{}'"
2727+ },
2728+ "status": {
2729+ "name": "status",
2730+ "type": "text",
2731+ "primaryKey": false,
2732+ "notNull": true,
2733+ "default": "'Approved'"
2734+ },
2735+ "is_cancer_gene": {
2736+ "name": "is_cancer_gene",
2737+ "type": "boolean",
2738+ "primaryKey": false,
2739+ "notNull": true,
2740+ "default": false
2741+ },
2742+ "civic_gene_id": {
2743+ "name": "civic_gene_id",
2744+ "type": "integer",
2745+ "primaryKey": false,
2746+ "notNull": false
2747+ },
2748+ "description": {
2749+ "name": "description",
2750+ "type": "text",
2751+ "primaryKey": false,
2752+ "notNull": false
2753+ },
2754+ "created_at": {
2755+ "name": "created_at",
2756+ "type": "timestamp with time zone",
2757+ "primaryKey": false,
2758+ "notNull": true,
2759+ "default": "now()"
2760+ },
2761+ "updated_at": {
2762+ "name": "updated_at",
2763+ "type": "timestamp with time zone",
2764+ "primaryKey": false,
2765+ "notNull": true,
2766+ "default": "now()"
2767+ }
2768+ },
2769+ "indexes": {
2770+ "genes_symbol_uq": {
2771+ "name": "genes_symbol_uq",
2772+ "columns": [
2773+ {
2774+ "expression": "symbol",
2775+ "isExpression": false,
2776+ "asc": true,
2777+ "nulls": "last"
2778+ }
2779+ ],
2780+ "isUnique": true,
2781+ "concurrently": false,
2782+ "method": "btree",
2783+ "with": {}
2784+ },
2785+ "genes_hgnc_uq": {
2786+ "name": "genes_hgnc_uq",
2787+ "columns": [
2788+ {
2789+ "expression": "hgnc_id",
2790+ "isExpression": false,
2791+ "asc": true,
2792+ "nulls": "last"
2793+ }
2794+ ],
2795+ "isUnique": true,
2796+ "concurrently": false,
2797+ "method": "btree",
2798+ "with": {}
2799+ },
2800+ "genes_ensembl_idx": {
2801+ "name": "genes_ensembl_idx",
2802+ "columns": [
2803+ {
2804+ "expression": "ensembl_gene_id",
2805+ "isExpression": false,
2806+ "asc": true,
2807+ "nulls": "last"
2808+ }
2809+ ],
2810+ "isUnique": false,
2811+ "concurrently": false,
2812+ "method": "btree",
2813+ "with": {}
2814+ },
2815+ "genes_ncbi_idx": {
2816+ "name": "genes_ncbi_idx",
2817+ "columns": [
2818+ {
2819+ "expression": "ncbi_gene_id",
2820+ "isExpression": false,
2821+ "asc": true,
2822+ "nulls": "last"
2823+ }
2824+ ],
2825+ "isUnique": false,
2826+ "concurrently": false,
2827+ "method": "btree",
2828+ "with": {}
2829+ }
2830+ },
2831+ "foreignKeys": {},
2832+ "compositePrimaryKeys": {},
2833+ "uniqueConstraints": {},
2834+ "policies": {},
2835+ "checkConstraints": {},
2836+ "isRLSEnabled": false
2837+ },
2838+ "public.genomic_cohorts": {
2839+ "name": "genomic_cohorts",
2840+ "schema": "",
2841+ "columns": {
2842+ "id": {
2843+ "name": "id",
2844+ "type": "varchar(32)",
2845+ "primaryKey": true,
2846+ "notNull": true
2847+ },
2848+ "source_id": {
2849+ "name": "source_id",
2850+ "type": "varchar(32)",
2851+ "primaryKey": false,
2852+ "notNull": true
2853+ },
2854+ "study_id": {
2855+ "name": "study_id",
2856+ "type": "text",
2857+ "primaryKey": false,
2858+ "notNull": true
2859+ },
2860+ "name": {
2861+ "name": "name",
2862+ "type": "text",
2863+ "primaryKey": false,
2864+ "notNull": true
2865+ },
2866+ "program": {
2867+ "name": "program",
2868+ "type": "text",
2869+ "primaryKey": false,
2870+ "notNull": false
2871+ },
2872+ "primary_sites": {
2873+ "name": "primary_sites",
2874+ "type": "text[]",
2875+ "primaryKey": false,
2876+ "notNull": true,
2877+ "default": "'{}'"
2878+ },
2879+ "disease_types": {
2880+ "name": "disease_types",
2881+ "type": "text[]",
2882+ "primaryKey": false,
2883+ "notNull": true,
2884+ "default": "'{}'"
2885+ },
2886+ "cancer_id": {
2887+ "name": "cancer_id",
2888+ "type": "varchar(32)",
2889+ "primaryKey": false,
2890+ "notNull": false
2891+ },
2892+ "cancer_match_type": {
2893+ "name": "cancer_match_type",
2894+ "type": "text",
2895+ "primaryKey": false,
2896+ "notNull": false
2897+ },
2898+ "case_count": {
2899+ "name": "case_count",
2900+ "type": "integer",
2901+ "primaryKey": false,
2902+ "notNull": false
2903+ },
2904+ "cases_with_ssm": {
2905+ "name": "cases_with_ssm",
2906+ "type": "integer",
2907+ "primaryKey": false,
2908+ "notNull": false
2909+ },
2910+ "data_release": {
2911+ "name": "data_release",
2912+ "type": "text",
2913+ "primaryKey": false,
2914+ "notNull": false
2915+ },
2916+ "access_level": {
2917+ "name": "access_level",
2918+ "type": "text",
2919+ "primaryKey": false,
2920+ "notNull": true,
2921+ "default": "'open'"
2922+ },
2923+ "url": {
2924+ "name": "url",
2925+ "type": "text",
2926+ "primaryKey": false,
2927+ "notNull": false
2928+ },
2929+ "provenance_id": {
2930+ "name": "provenance_id",
2931+ "type": "integer",
2932+ "primaryKey": false,
2933+ "notNull": false
2934+ },
2935+ "updated_at": {
2936+ "name": "updated_at",
2937+ "type": "timestamp with time zone",
2938+ "primaryKey": false,
2939+ "notNull": true,
2940+ "default": "now()"
2941+ }
2942+ },
2943+ "indexes": {
2944+ "genomic_cohorts_uq": {
2945+ "name": "genomic_cohorts_uq",
2946+ "columns": [
2947+ {
2948+ "expression": "source_id",
2949+ "isExpression": false,
2950+ "asc": true,
2951+ "nulls": "last"
2952+ },
2953+ {
2954+ "expression": "study_id",
2955+ "isExpression": false,
2956+ "asc": true,
2957+ "nulls": "last"
2958+ }
2959+ ],
2960+ "isUnique": true,
2961+ "concurrently": false,
2962+ "method": "btree",
2963+ "with": {}
2964+ },
2965+ "genomic_cohorts_cancer_idx": {
2966+ "name": "genomic_cohorts_cancer_idx",
2967+ "columns": [
2968+ {
2969+ "expression": "cancer_id",
2970+ "isExpression": false,
2971+ "asc": true,
2972+ "nulls": "last"
2973+ }
2974+ ],
2975+ "isUnique": false,
2976+ "concurrently": false,
2977+ "method": "btree",
2978+ "with": {}
2979+ }
2980+ },
2981+ "foreignKeys": {},
2982+ "compositePrimaryKeys": {},
2983+ "uniqueConstraints": {},
2984+ "policies": {},
2985+ "checkConstraints": {},
2986+ "isRLSEnabled": false
2987+ },
2988+ "public.variant_aliases": {
2989+ "name": "variant_aliases",
2990+ "schema": "",
2991+ "columns": {
2992+ "id": {
2993+ "name": "id",
2994+ "type": "bigserial",
2995+ "primaryKey": true,
2996+ "notNull": true
2997+ },
2998+ "variant_id": {
2999+ "name": "variant_id",
3000+ "type": "varchar(32)",
3001+ "primaryKey": false,
3002+ "notNull": true
3003+ },
3004+ "alias": {
3005+ "name": "alias",
3006+ "type": "text",
3007+ "primaryKey": false,
3008+ "notNull": true
3009+ },
3010+ "source_id": {
3011+ "name": "source_id",
3012+ "type": "varchar(32)",
3013+ "primaryKey": false,
3014+ "notNull": false
3015+ }
3016+ },
3017+ "indexes": {
3018+ "variant_aliases_uq": {
3019+ "name": "variant_aliases_uq",
3020+ "columns": [
3021+ {
3022+ "expression": "variant_id",
3023+ "isExpression": false,
3024+ "asc": true,
3025+ "nulls": "last"
3026+ },
3027+ {
3028+ "expression": "alias",
3029+ "isExpression": false,
3030+ "asc": true,
3031+ "nulls": "last"
3032+ }
3033+ ],
3034+ "isUnique": true,
3035+ "concurrently": false,
3036+ "method": "btree",
3037+ "with": {}
3038+ }
3039+ },
3040+ "foreignKeys": {},
3041+ "compositePrimaryKeys": {},
3042+ "uniqueConstraints": {},
3043+ "policies": {},
3044+ "checkConstraints": {},
3045+ "isRLSEnabled": false
3046+ },
3047+ "public.variant_clinical_significance": {
3048+ "name": "variant_clinical_significance",
3049+ "schema": "",
3050+ "columns": {
3051+ "id": {
3052+ "name": "id",
3053+ "type": "bigserial",
3054+ "primaryKey": true,
3055+ "notNull": true
3056+ },
3057+ "variant_id": {
3058+ "name": "variant_id",
3059+ "type": "varchar(32)",
3060+ "primaryKey": false,
3061+ "notNull": true
3062+ },
3063+ "clinvar_variation_id": {
3064+ "name": "clinvar_variation_id",
3065+ "type": "text",
3066+ "primaryKey": false,
3067+ "notNull": true
3068+ },
3069+ "clinical_significance": {
3070+ "name": "clinical_significance",
3071+ "type": "text",
3072+ "primaryKey": false,
3073+ "notNull": true
3074+ },
3075+ "review_status": {
3076+ "name": "review_status",
3077+ "type": "text",
3078+ "primaryKey": false,
3079+ "notNull": false
3080+ },
3081+ "star_rating": {
3082+ "name": "star_rating",
3083+ "type": "integer",
3084+ "primaryKey": false,
3085+ "notNull": false
3086+ },
3087+ "last_evaluated": {
3088+ "name": "last_evaluated",
3089+ "type": "text",
3090+ "primaryKey": false,
3091+ "notNull": false
3092+ },
3093+ "conditions": {
3094+ "name": "conditions",
3095+ "type": "text[]",
3096+ "primaryKey": false,
3097+ "notNull": true,
3098+ "default": "'{}'"
3099+ },
3100+ "condition_cancer_ids": {
3101+ "name": "condition_cancer_ids",
3102+ "type": "text[]",
3103+ "primaryKey": false,
3104+ "notNull": true,
3105+ "default": "'{}'"
3106+ },
3107+ "origin_simple": {
3108+ "name": "origin_simple",
3109+ "type": "text",
3110+ "primaryKey": false,
3111+ "notNull": false
3112+ },
3113+ "number_submitters": {
3114+ "name": "number_submitters",
3115+ "type": "integer",
3116+ "primaryKey": false,
3117+ "notNull": false
3118+ },
3119+ "provenance_id": {
3120+ "name": "provenance_id",
3121+ "type": "integer",
3122+ "primaryKey": false,
3123+ "notNull": true
3124+ },
3125+ "ingest_run_id": {
3126+ "name": "ingest_run_id",
3127+ "type": "text",
3128+ "primaryKey": false,
3129+ "notNull": false
3130+ },
3131+ "updated_at": {
3132+ "name": "updated_at",
3133+ "type": "timestamp with time zone",
3134+ "primaryKey": false,
3135+ "notNull": true,
3136+ "default": "now()"
3137+ }
3138+ },
3139+ "indexes": {
3140+ "variant_clinsig_uq": {
3141+ "name": "variant_clinsig_uq",
3142+ "columns": [
3143+ {
3144+ "expression": "clinvar_variation_id",
3145+ "isExpression": false,
3146+ "asc": true,
3147+ "nulls": "last"
3148+ }
3149+ ],
3150+ "isUnique": true,
3151+ "concurrently": false,
3152+ "method": "btree",
3153+ "with": {}
3154+ }
3155+ },
3156+ "foreignKeys": {},
3157+ "compositePrimaryKeys": {},
3158+ "uniqueConstraints": {},
3159+ "policies": {},
3160+ "checkConstraints": {},
3161+ "isRLSEnabled": false
3162+ },
3163+ "public.variants": {
3164+ "name": "variants",
3165+ "schema": "",
3166+ "columns": {
3167+ "id": {
3168+ "name": "id",
3169+ "type": "varchar(32)",
3170+ "primaryKey": true,
3171+ "notNull": true
3172+ },
3173+ "slug": {
3174+ "name": "slug",
3175+ "type": "text",
3176+ "primaryKey": false,
3177+ "notNull": true
3178+ },
3179+ "gene_id": {
3180+ "name": "gene_id",
3181+ "type": "varchar(32)",
3182+ "primaryKey": false,
3183+ "notNull": false
3184+ },
3185+ "gene_symbol": {
3186+ "name": "gene_symbol",
3187+ "type": "text",
3188+ "primaryKey": false,
3189+ "notNull": false
3190+ },
3191+ "name": {
3192+ "name": "name",
3193+ "type": "text",
3194+ "primaryKey": false,
3195+ "notNull": true
3196+ },
3197+ "variant_type": {
3198+ "name": "variant_type",
3199+ "type": "text",
3200+ "primaryKey": false,
3201+ "notNull": false
3202+ },
3203+ "hgvs_g": {
3204+ "name": "hgvs_g",
3205+ "type": "text",
3206+ "primaryKey": false,
3207+ "notNull": false
3208+ },
3209+ "hgvs_c": {
3210+ "name": "hgvs_c",
3211+ "type": "text",
3212+ "primaryKey": false,
3213+ "notNull": false
3214+ },
3215+ "hgvs_p": {
3216+ "name": "hgvs_p",
3217+ "type": "text",
3218+ "primaryKey": false,
3219+ "notNull": false
3220+ },
3221+ "assembly": {
3222+ "name": "assembly",
3223+ "type": "text",
3224+ "primaryKey": false,
3225+ "notNull": false
3226+ },
3227+ "chromosome": {
3228+ "name": "chromosome",
3229+ "type": "text",
3230+ "primaryKey": false,
3231+ "notNull": false
3232+ },
3233+ "start": {
3234+ "name": "start",
3235+ "type": "integer",
3236+ "primaryKey": false,
3237+ "notNull": false
3238+ },
3239+ "end": {
3240+ "name": "end",
3241+ "type": "integer",
3242+ "primaryKey": false,
3243+ "notNull": false
3244+ },
3245+ "reference_bases": {
3246+ "name": "reference_bases",
3247+ "type": "text",
3248+ "primaryKey": false,
3249+ "notNull": false
3250+ },
3251+ "alternate_bases": {
3252+ "name": "alternate_bases",
3253+ "type": "text",
3254+ "primaryKey": false,
3255+ "notNull": false
3256+ },
3257+ "coordinates": {
3258+ "name": "coordinates",
3259+ "type": "jsonb",
3260+ "primaryKey": false,
3261+ "notNull": true,
3262+ "default": "'[]'::jsonb"
3263+ },
3264+ "clinvar_variation_id": {
3265+ "name": "clinvar_variation_id",
3266+ "type": "text",
3267+ "primaryKey": false,
3268+ "notNull": false
3269+ },
3270+ "civic_variant_id": {
3271+ "name": "civic_variant_id",
3272+ "type": "integer",
3273+ "primaryKey": false,
3274+ "notNull": false
3275+ },
3276+ "dbsnp_ids": {
3277+ "name": "dbsnp_ids",
3278+ "type": "text[]",
3279+ "primaryKey": false,
3280+ "notNull": true,
3281+ "default": "'{}'"
3282+ },
3283+ "fusion_partners": {
3284+ "name": "fusion_partners",
3285+ "type": "text[]",
3286+ "primaryKey": false,
3287+ "notNull": true,
3288+ "default": "'{}'"
3289+ },
3290+ "created_at": {
3291+ "name": "created_at",
3292+ "type": "timestamp with time zone",
3293+ "primaryKey": false,
3294+ "notNull": true,
3295+ "default": "now()"
3296+ },
3297+ "updated_at": {
3298+ "name": "updated_at",
3299+ "type": "timestamp with time zone",
3300+ "primaryKey": false,
3301+ "notNull": true,
3302+ "default": "now()"
3303+ }
3304+ },
3305+ "indexes": {
3306+ "variants_slug_uq": {
3307+ "name": "variants_slug_uq",
3308+ "columns": [
3309+ {
3310+ "expression": "slug",
3311+ "isExpression": false,
3312+ "asc": true,
3313+ "nulls": "last"
3314+ }
3315+ ],
3316+ "isUnique": true,
3317+ "concurrently": false,
3318+ "method": "btree",
3319+ "with": {}
3320+ },
3321+ "variants_gene_idx": {
3322+ "name": "variants_gene_idx",
3323+ "columns": [
3324+ {
3325+ "expression": "gene_id",
3326+ "isExpression": false,
3327+ "asc": true,
3328+ "nulls": "last"
3329+ }
3330+ ],
3331+ "isUnique": false,
3332+ "concurrently": false,
3333+ "method": "btree",
3334+ "with": {}
3335+ },
3336+ "variants_clinvar_idx": {
3337+ "name": "variants_clinvar_idx",
3338+ "columns": [
3339+ {
3340+ "expression": "clinvar_variation_id",
3341+ "isExpression": false,
3342+ "asc": true,
3343+ "nulls": "last"
3344+ }
3345+ ],
3346+ "isUnique": false,
3347+ "concurrently": false,
3348+ "method": "btree",
3349+ "with": {}
3350+ },
3351+ "variants_civic_idx": {
3352+ "name": "variants_civic_idx",
3353+ "columns": [
3354+ {
3355+ "expression": "civic_variant_id",
3356+ "isExpression": false,
3357+ "asc": true,
3358+ "nulls": "last"
3359+ }
3360+ ],
3361+ "isUnique": false,
3362+ "concurrently": false,
3363+ "method": "btree",
3364+ "with": {}
3365+ }
3366+ },
3367+ "foreignKeys": {},
3368+ "compositePrimaryKeys": {},
3369+ "uniqueConstraints": {},
3370+ "policies": {},
3371+ "checkConstraints": {},
3372+ "isRLSEnabled": false
3373+ },
3374+ "public.drug_aliases": {
3375+ "name": "drug_aliases",
3376+ "schema": "",
3377+ "columns": {
3378+ "id": {
3379+ "name": "id",
3380+ "type": "bigserial",
3381+ "primaryKey": true,
3382+ "notNull": true
3383+ },
3384+ "drug_id": {
3385+ "name": "drug_id",
3386+ "type": "varchar(32)",
3387+ "primaryKey": false,
3388+ "notNull": true
3389+ },
3390+ "alias": {
3391+ "name": "alias",
3392+ "type": "text",
3393+ "primaryKey": false,
3394+ "notNull": true
3395+ },
3396+ "normalized": {
3397+ "name": "normalized",
3398+ "type": "text",
3399+ "primaryKey": false,
3400+ "notNull": true
3401+ },
3402+ "alias_type": {
3403+ "name": "alias_type",
3404+ "type": "text",
3405+ "primaryKey": false,
3406+ "notNull": true,
3407+ "default": "'synonym'"
3408+ },
3409+ "source_id": {
3410+ "name": "source_id",
3411+ "type": "varchar(32)",
3412+ "primaryKey": false,
3413+ "notNull": false
3414+ }
3415+ },
3416+ "indexes": {
3417+ "drug_aliases_uq": {
3418+ "name": "drug_aliases_uq",
3419+ "columns": [
3420+ {
3421+ "expression": "drug_id",
3422+ "isExpression": false,
3423+ "asc": true,
3424+ "nulls": "last"
3425+ },
3426+ {
3427+ "expression": "normalized",
3428+ "isExpression": false,
3429+ "asc": true,
3430+ "nulls": "last"
3431+ },
3432+ {
3433+ "expression": "alias_type",
3434+ "isExpression": false,
3435+ "asc": true,
3436+ "nulls": "last"
3437+ }
3438+ ],
3439+ "isUnique": true,
3440+ "concurrently": false,
3441+ "method": "btree",
3442+ "with": {}
3443+ },
3444+ "drug_aliases_norm_idx": {
3445+ "name": "drug_aliases_norm_idx",
3446+ "columns": [
3447+ {
3448+ "expression": "normalized",
3449+ "isExpression": false,
3450+ "asc": true,
3451+ "nulls": "last"
3452+ }
3453+ ],
3454+ "isUnique": false,
3455+ "concurrently": false,
3456+ "method": "btree",
3457+ "with": {}
3458+ }
3459+ },
3460+ "foreignKeys": {},
3461+ "compositePrimaryKeys": {},
3462+ "uniqueConstraints": {},
3463+ "policies": {},
3464+ "checkConstraints": {},
3465+ "isRLSEnabled": false
3466+ },
3467+ "public.drug_approvals": {
3468+ "name": "drug_approvals",
3469+ "schema": "",
3470+ "columns": {
3471+ "id": {
3472+ "name": "id",
3473+ "type": "bigserial",
3474+ "primaryKey": true,
3475+ "notNull": true
3476+ },
3477+ "drug_id": {
3478+ "name": "drug_id",
3479+ "type": "varchar(32)",
3480+ "primaryKey": false,
3481+ "notNull": true
3482+ },
3483+ "cancer_id": {
3484+ "name": "cancer_id",
3485+ "type": "varchar(32)",
3486+ "primaryKey": false,
3487+ "notNull": false
3488+ },
3489+ "biomarker_ids": {
3490+ "name": "biomarker_ids",
3491+ "type": "text[]",
3492+ "primaryKey": false,
3493+ "notNull": true,
3494+ "default": "'{}'"
3495+ },
3496+ "tumor_agnostic": {
3497+ "name": "tumor_agnostic",
3498+ "type": "boolean",
3499+ "primaryKey": false,
3500+ "notNull": true,
3501+ "default": false
3502+ },
3503+ "jurisdiction": {
3504+ "name": "jurisdiction",
3505+ "type": "text",
3506+ "primaryKey": false,
3507+ "notNull": true
3508+ },
3509+ "authority": {
3510+ "name": "authority",
3511+ "type": "text",
3512+ "primaryKey": false,
3513+ "notNull": true
3514+ },
3515+ "indication": {
3516+ "name": "indication",
3517+ "type": "text",
3518+ "primaryKey": false,
3519+ "notNull": true
3520+ },
3521+ "line_of_therapy": {
3522+ "name": "line_of_therapy",
3523+ "type": "text",
3524+ "primaryKey": false,
3525+ "notNull": false
3526+ },
3527+ "disease_stage": {
3528+ "name": "disease_stage",
3529+ "type": "text",
3530+ "primaryKey": false,
3531+ "notNull": false
3532+ },
3533+ "approval_type": {
3534+ "name": "approval_type",
3535+ "type": "text",
3536+ "primaryKey": false,
3537+ "notNull": false
3538+ },
3539+ "accelerated": {
3540+ "name": "accelerated",
3541+ "type": "boolean",
3542+ "primaryKey": false,
3543+ "notNull": false
3544+ },
3545+ "conditional": {
3546+ "name": "conditional",
3547+ "type": "boolean",
3548+ "primaryKey": false,
3549+ "notNull": false
3550+ },
3551+ "approval_date": {
3552+ "name": "approval_date",
3553+ "type": "text",
3554+ "primaryKey": false,
3555+ "notNull": false
3556+ },
3557+ "withdrawal_date": {
3558+ "name": "withdrawal_date",
3559+ "type": "text",
3560+ "primaryKey": false,
3561+ "notNull": false
3562+ },
3563+ "status": {
3564+ "name": "status",
3565+ "type": "text",
3566+ "primaryKey": false,
3567+ "notNull": true
3568+ },
3569+ "application_number": {
3570+ "name": "application_number",
3571+ "type": "text",
3572+ "primaryKey": false,
3573+ "notNull": false
3574+ },
3575+ "source_id": {
3576+ "name": "source_id",
3577+ "type": "varchar(32)",
3578+ "primaryKey": false,
3579+ "notNull": true
3580+ },
3581+ "provenance_id": {
3582+ "name": "provenance_id",
3583+ "type": "integer",
3584+ "primaryKey": false,
3585+ "notNull": true
3586+ },
3587+ "raw": {
3588+ "name": "raw",
3589+ "type": "jsonb",
3590+ "primaryKey": false,
3591+ "notNull": false
3592+ },
3593+ "created_at": {
3594+ "name": "created_at",
3595+ "type": "timestamp with time zone",
3596+ "primaryKey": false,
3597+ "notNull": true,
3598+ "default": "now()"
3599+ },
3600+ "updated_at": {
3601+ "name": "updated_at",
3602+ "type": "timestamp with time zone",
3603+ "primaryKey": false,
3604+ "notNull": true,
3605+ "default": "now()"
3606+ }
3607+ },
3608+ "indexes": {
3609+ "drug_approvals_drug_idx": {
3610+ "name": "drug_approvals_drug_idx",
3611+ "columns": [
3612+ {
3613+ "expression": "drug_id",
3614+ "isExpression": false,
3615+ "asc": true,
3616+ "nulls": "last"
3617+ }
3618+ ],
3619+ "isUnique": false,
3620+ "concurrently": false,
3621+ "method": "btree",
3622+ "with": {}
3623+ },
3624+ "drug_approvals_cancer_idx": {
3625+ "name": "drug_approvals_cancer_idx",
3626+ "columns": [
3627+ {
3628+ "expression": "cancer_id",
3629+ "isExpression": false,
3630+ "asc": true,
3631+ "nulls": "last"
3632+ }
3633+ ],
3634+ "isUnique": false,
3635+ "concurrently": false,
3636+ "method": "btree",
3637+ "with": {}
3638+ }
3639+ },
3640+ "foreignKeys": {},
3641+ "compositePrimaryKeys": {},
3642+ "uniqueConstraints": {},
3643+ "policies": {},
3644+ "checkConstraints": {},
3645+ "isRLSEnabled": false
3646+ },
3647+ "public.drugs": {
3648+ "name": "drugs",
3649+ "schema": "",
3650+ "columns": {
3651+ "id": {
3652+ "name": "id",
3653+ "type": "varchar(32)",
3654+ "primaryKey": true,
3655+ "notNull": true
3656+ },
3657+ "slug": {
3658+ "name": "slug",
3659+ "type": "text",
3660+ "primaryKey": false,
3661+ "notNull": true
3662+ },
3663+ "name": {
3664+ "name": "name",
3665+ "type": "text",
3666+ "primaryKey": false,
3667+ "notNull": true
3668+ },
3669+ "kind": {
3670+ "name": "kind",
3671+ "type": "text",
3672+ "primaryKey": false,
3673+ "notNull": false
3674+ },
3675+ "ncit_code": {
3676+ "name": "ncit_code",
3677+ "type": "text",
3678+ "primaryKey": false,
3679+ "notNull": false
3680+ },
3681+ "chembl_id": {
3682+ "name": "chembl_id",
3683+ "type": "text",
3684+ "primaryKey": false,
3685+ "notNull": false
3686+ },
3687+ "civic_therapy_id": {
3688+ "name": "civic_therapy_id",
3689+ "type": "integer",
3690+ "primaryKey": false,
3691+ "notNull": false
3692+ },
3693+ "drugbank_id": {
3694+ "name": "drugbank_id",
3695+ "type": "text",
3696+ "primaryKey": false,
3697+ "notNull": false
3698+ },
3699+ "pubchem_cid": {
3700+ "name": "pubchem_cid",
3701+ "type": "text",
3702+ "primaryKey": false,
3703+ "notNull": false
3704+ },
3705+ "unii": {
3706+ "name": "unii",
3707+ "type": "text",
3708+ "primaryKey": false,
3709+ "notNull": false
3710+ },
3711+ "mechanism": {
3712+ "name": "mechanism",
3713+ "type": "text",
3714+ "primaryKey": false,
3715+ "notNull": false
3716+ },
3717+ "target_gene_ids": {
3718+ "name": "target_gene_ids",
3719+ "type": "text[]",
3720+ "primaryKey": false,
3721+ "notNull": true,
3722+ "default": "'{}'"
3723+ },
3724+ "development_status": {
3725+ "name": "development_status",
3726+ "type": "text",
3727+ "primaryKey": false,
3728+ "notNull": false
3729+ },
3730+ "description": {
3731+ "name": "description",
3732+ "type": "text",
3733+ "primaryKey": false,
3734+ "notNull": false
3735+ },
3736+ "created_at": {
3737+ "name": "created_at",
3738+ "type": "timestamp with time zone",
3739+ "primaryKey": false,
3740+ "notNull": true,
3741+ "default": "now()"
3742+ },
3743+ "updated_at": {
3744+ "name": "updated_at",
3745+ "type": "timestamp with time zone",
3746+ "primaryKey": false,
3747+ "notNull": true,
3748+ "default": "now()"
3749+ }
3750+ },
3751+ "indexes": {
3752+ "drugs_slug_uq": {
3753+ "name": "drugs_slug_uq",
3754+ "columns": [
3755+ {
3756+ "expression": "slug",
3757+ "isExpression": false,
3758+ "asc": true,
3759+ "nulls": "last"
3760+ }
3761+ ],
3762+ "isUnique": true,
3763+ "concurrently": false,
3764+ "method": "btree",
3765+ "with": {}
3766+ },
3767+ "drugs_ncit_idx": {
3768+ "name": "drugs_ncit_idx",
3769+ "columns": [
3770+ {
3771+ "expression": "ncit_code",
3772+ "isExpression": false,
3773+ "asc": true,
3774+ "nulls": "last"
3775+ }
3776+ ],
3777+ "isUnique": false,
3778+ "concurrently": false,
3779+ "method": "btree",
3780+ "with": {}
3781+ },
3782+ "drugs_civic_idx": {
3783+ "name": "drugs_civic_idx",
3784+ "columns": [
3785+ {
3786+ "expression": "civic_therapy_id",
3787+ "isExpression": false,
3788+ "asc": true,
3789+ "nulls": "last"
3790+ }
3791+ ],
3792+ "isUnique": false,
3793+ "concurrently": false,
3794+ "method": "btree",
3795+ "with": {}
3796+ },
3797+ "drugs_chembl_idx": {
3798+ "name": "drugs_chembl_idx",
3799+ "columns": [
3800+ {
3801+ "expression": "chembl_id",
3802+ "isExpression": false,
3803+ "asc": true,
3804+ "nulls": "last"
3805+ }
3806+ ],
3807+ "isUnique": false,
3808+ "concurrently": false,
3809+ "method": "btree",
3810+ "with": {}
3811+ }
3812+ },
3813+ "foreignKeys": {},
3814+ "compositePrimaryKeys": {},
3815+ "uniqueConstraints": {},
3816+ "policies": {},
3817+ "checkConstraints": {},
3818+ "isRLSEnabled": false
3819+ },
3820+ "public.treatment_regimens": {
3821+ "name": "treatment_regimens",
3822+ "schema": "",
3823+ "columns": {
3824+ "id": {
3825+ "name": "id",
3826+ "type": "varchar(32)",
3827+ "primaryKey": true,
3828+ "notNull": true
3829+ },
3830+ "slug": {
3831+ "name": "slug",
3832+ "type": "text",
3833+ "primaryKey": false,
3834+ "notNull": true
3835+ },
3836+ "name": {
3837+ "name": "name",
3838+ "type": "text",
3839+ "primaryKey": false,
3840+ "notNull": true
3841+ },
3842+ "component_drug_ids": {
3843+ "name": "component_drug_ids",
3844+ "type": "text[]",
3845+ "primaryKey": false,
3846+ "notNull": true,
3847+ "default": "'{}'"
3848+ },
3849+ "modality": {
3850+ "name": "modality",
3851+ "type": "text",
3852+ "primaryKey": false,
3853+ "notNull": true,
3854+ "default": "'drug_combination'"
3855+ },
3856+ "description": {
3857+ "name": "description",
3858+ "type": "text",
3859+ "primaryKey": false,
3860+ "notNull": false
3861+ },
3862+ "created_at": {
3863+ "name": "created_at",
3864+ "type": "timestamp with time zone",
3865+ "primaryKey": false,
3866+ "notNull": true,
3867+ "default": "now()"
3868+ }
3869+ },
3870+ "indexes": {
3871+ "treatment_regimens_slug_uq": {
3872+ "name": "treatment_regimens_slug_uq",
3873+ "columns": [
3874+ {
3875+ "expression": "slug",
3876+ "isExpression": false,
3877+ "asc": true,
3878+ "nulls": "last"
3879+ }
3880+ ],
3881+ "isUnique": true,
3882+ "concurrently": false,
3883+ "method": "btree",
3884+ "with": {}
3885+ }
3886+ },
3887+ "foreignKeys": {},
3888+ "compositePrimaryKeys": {},
3889+ "uniqueConstraints": {},
3890+ "policies": {},
3891+ "checkConstraints": {},
3892+ "isRLSEnabled": false
3893+ },
3894+ "public.clinical_trials": {
3895+ "name": "clinical_trials",
3896+ "schema": "",
3897+ "columns": {
3898+ "id": {
3899+ "name": "id",
3900+ "type": "varchar(32)",
3901+ "primaryKey": true,
3902+ "notNull": true
3903+ },
3904+ "nct_id": {
3905+ "name": "nct_id",
3906+ "type": "text",
3907+ "primaryKey": false,
3908+ "notNull": true
3909+ },
3910+ "brief_title": {
3911+ "name": "brief_title",
3912+ "type": "text",
3913+ "primaryKey": false,
3914+ "notNull": true
3915+ },
3916+ "official_title": {
3917+ "name": "official_title",
3918+ "type": "text",
3919+ "primaryKey": false,
3920+ "notNull": false
3921+ },
3922+ "acronym": {
3923+ "name": "acronym",
3924+ "type": "text",
3925+ "primaryKey": false,
3926+ "notNull": false
3927+ },
3928+ "study_type": {
3929+ "name": "study_type",
3930+ "type": "text",
3931+ "primaryKey": false,
3932+ "notNull": false
3933+ },
3934+ "phases": {
3935+ "name": "phases",
3936+ "type": "text[]",
3937+ "primaryKey": false,
3938+ "notNull": true,
3939+ "default": "'{}'"
3940+ },
3941+ "overall_status": {
3942+ "name": "overall_status",
3943+ "type": "text",
3944+ "primaryKey": false,
3945+ "notNull": false
3946+ },
3947+ "why_stopped": {
3948+ "name": "why_stopped",
3949+ "type": "text",
3950+ "primaryKey": false,
3951+ "notNull": false
3952+ },
3953+ "start_date": {
3954+ "name": "start_date",
3955+ "type": "text",
3956+ "primaryKey": false,
3957+ "notNull": false
3958+ },
3959+ "primary_completion_date": {
3960+ "name": "primary_completion_date",
3961+ "type": "text",
3962+ "primaryKey": false,
3963+ "notNull": false
3964+ },
3965+ "completion_date": {
3966+ "name": "completion_date",
3967+ "type": "text",
3968+ "primaryKey": false,
3969+ "notNull": false
3970+ },
3971+ "first_posted_date": {
3972+ "name": "first_posted_date",
3973+ "type": "text",
3974+ "primaryKey": false,
3975+ "notNull": false
3976+ },
3977+ "last_update_posted_date": {
3978+ "name": "last_update_posted_date",
3979+ "type": "text",
3980+ "primaryKey": false,
3981+ "notNull": false
3982+ },
3983+ "results_first_posted_date": {
3984+ "name": "results_first_posted_date",
3985+ "type": "text",
3986+ "primaryKey": false,
3987+ "notNull": false
3988+ },
3989+ "has_results": {
3990+ "name": "has_results",
3991+ "type": "boolean",
3992+ "primaryKey": false,
3993+ "notNull": true,
3994+ "default": false
3995+ },
3996+ "enrollment_count": {
3997+ "name": "enrollment_count",
3998+ "type": "integer",
3999+ "primaryKey": false,
4000+ "notNull": false
4001+ },
4002+ "enrollment_type": {
4003+ "name": "enrollment_type",
4004+ "type": "text",
4005+ "primaryKey": false,
4006+ "notNull": false
4007+ },
4008+ "lead_sponsor": {
4009+ "name": "lead_sponsor",
4010+ "type": "text",
4011+ "primaryKey": false,
4012+ "notNull": false
4013+ },
4014+ "lead_sponsor_class": {
4015+ "name": "lead_sponsor_class",
4016+ "type": "text",
4017+ "primaryKey": false,
4018+ "notNull": false
4019+ },
4020+ "collaborators": {
4021+ "name": "collaborators",
4022+ "type": "text[]",
4023+ "primaryKey": false,
4024+ "notNull": true,
4025+ "default": "'{}'"
4026+ },
4027+ "conditions": {
4028+ "name": "conditions",
4029+ "type": "text[]",
4030+ "primaryKey": false,
4031+ "notNull": true,
4032+ "default": "'{}'"
4033+ },
4034+ "keywords": {
4035+ "name": "keywords",
4036+ "type": "text[]",
4037+ "primaryKey": false,
4038+ "notNull": true,
4039+ "default": "'{}'"
4040+ },
4041+ "interventions": {
4042+ "name": "interventions",
4043+ "type": "jsonb",
4044+ "primaryKey": false,
4045+ "notNull": true,
4046+ "default": "'[]'::jsonb"
4047+ },
4048+ "arms": {
4049+ "name": "arms",
4050+ "type": "jsonb",
4051+ "primaryKey": false,
4052+ "notNull": true,
4053+ "default": "'[]'::jsonb"
4054+ },
4055+ "primary_outcomes": {
4056+ "name": "primary_outcomes",
4057+ "type": "jsonb",
4058+ "primaryKey": false,
4059+ "notNull": true,
4060+ "default": "'[]'::jsonb"
4061+ },
4062+ "secondary_outcomes": {
4063+ "name": "secondary_outcomes",
4064+ "type": "jsonb",
4065+ "primaryKey": false,
4066+ "notNull": true,
4067+ "default": "'[]'::jsonb"
4068+ },
4069+ "eligibility": {
4070+ "name": "eligibility",
4071+ "type": "jsonb",
4072+ "primaryKey": false,
4073+ "notNull": true,
4074+ "default": "'{}'::jsonb"
4075+ },
4076+ "sex": {
4077+ "name": "sex",
4078+ "type": "text",
4079+ "primaryKey": false,
4080+ "notNull": false
4081+ },
4082+ "minimum_age": {
4083+ "name": "minimum_age",
4084+ "type": "text",
4085+ "primaryKey": false,
4086+ "notNull": false
4087+ },
4088+ "maximum_age": {
4089+ "name": "maximum_age",
4090+ "type": "text",
4091+ "primaryKey": false,
4092+ "notNull": false
4093+ },
4094+ "countries": {
4095+ "name": "countries",
4096+ "type": "text[]",
4097+ "primaryKey": false,
4098+ "notNull": true,
4099+ "default": "'{}'"
4100+ },
4101+ "locations_count": {
4102+ "name": "locations_count",
4103+ "type": "integer",
4104+ "primaryKey": false,
4105+ "notNull": true,
4106+ "default": 0
4107+ },
4108+ "references": {
4109+ "name": "references",
4110+ "type": "jsonb",
4111+ "primaryKey": false,
4112+ "notNull": true,
4113+ "default": "'[]'::jsonb"
4114+ },
4115+ "brief_summary": {
4116+ "name": "brief_summary",
4117+ "type": "text",
4118+ "primaryKey": false,
4119+ "notNull": false
4120+ },
4121+ "is_oncology": {
4122+ "name": "is_oncology",
4123+ "type": "boolean",
4124+ "primaryKey": false,
4125+ "notNull": true,
4126+ "default": true
4127+ },
4128+ "source_record_id": {
4129+ "name": "source_record_id",
4130+ "type": "integer",
4131+ "primaryKey": false,
4132+ "notNull": false
4133+ },
4134+ "ingest_run_id": {
4135+ "name": "ingest_run_id",
4136+ "type": "text",
4137+ "primaryKey": false,
4138+ "notNull": false
4139+ },
4140+ "created_at": {
4141+ "name": "created_at",
4142+ "type": "timestamp with time zone",
4143+ "primaryKey": false,
4144+ "notNull": true,
4145+ "default": "now()"
4146+ },
4147+ "updated_at": {
4148+ "name": "updated_at",
4149+ "type": "timestamp with time zone",
4150+ "primaryKey": false,
4151+ "notNull": true,
4152+ "default": "now()"
4153+ }
4154+ },
4155+ "indexes": {
4156+ "clinical_trials_nct_uq": {
4157+ "name": "clinical_trials_nct_uq",
4158+ "columns": [
4159+ {
4160+ "expression": "nct_id",
4161+ "isExpression": false,
4162+ "asc": true,
4163+ "nulls": "last"
4164+ }
4165+ ],
4166+ "isUnique": true,
4167+ "concurrently": false,
4168+ "method": "btree",
4169+ "with": {}
4170+ },
4171+ "clinical_trials_status_idx": {
4172+ "name": "clinical_trials_status_idx",
4173+ "columns": [
4174+ {
4175+ "expression": "overall_status",
4176+ "isExpression": false,
4177+ "asc": true,
4178+ "nulls": "last"
4179+ }
4180+ ],
4181+ "isUnique": false,
4182+ "concurrently": false,
4183+ "method": "btree",
4184+ "with": {}
4185+ },
4186+ "clinical_trials_updated_idx": {
4187+ "name": "clinical_trials_updated_idx",
4188+ "columns": [
4189+ {
4190+ "expression": "last_update_posted_date",
4191+ "isExpression": false,
4192+ "asc": true,
4193+ "nulls": "last"
4194+ }
4195+ ],
4196+ "isUnique": false,
4197+ "concurrently": false,
4198+ "method": "btree",
4199+ "with": {}
4200+ },
4201+ "clinical_trials_sponsor_idx": {
4202+ "name": "clinical_trials_sponsor_idx",
4203+ "columns": [
4204+ {
4205+ "expression": "lead_sponsor",
4206+ "isExpression": false,
4207+ "asc": true,
4208+ "nulls": "last"
4209+ }
4210+ ],
4211+ "isUnique": false,
4212+ "concurrently": false,
4213+ "method": "btree",
4214+ "with": {}
4215+ }
4216+ },
4217+ "foreignKeys": {},
4218+ "compositePrimaryKeys": {},
4219+ "uniqueConstraints": {},
4220+ "policies": {},
4221+ "checkConstraints": {},
4222+ "isRLSEnabled": false
4223+ },
4224+ "public.trial_conditions": {
4225+ "name": "trial_conditions",
4226+ "schema": "",
4227+ "columns": {
4228+ "id": {
4229+ "name": "id",
4230+ "type": "bigserial",
4231+ "primaryKey": true,
4232+ "notNull": true
4233+ },
4234+ "trial_id": {
4235+ "name": "trial_id",
4236+ "type": "varchar(32)",
4237+ "primaryKey": false,
4238+ "notNull": true
4239+ },
4240+ "condition_text": {
4241+ "name": "condition_text",
4242+ "type": "text",
4243+ "primaryKey": false,
4244+ "notNull": true
4245+ },
4246+ "normalized": {
4247+ "name": "normalized",
4248+ "type": "text",
4249+ "primaryKey": false,
4250+ "notNull": true
4251+ },
4252+ "cancer_id": {
4253+ "name": "cancer_id",
4254+ "type": "varchar(32)",
4255+ "primaryKey": false,
4256+ "notNull": false
4257+ },
4258+ "match_type": {
4259+ "name": "match_type",
4260+ "type": "text",
4261+ "primaryKey": false,
4262+ "notNull": true,
4263+ "default": "'UNRESOLVED'"
4264+ },
4265+ "confidence": {
4266+ "name": "confidence",
4267+ "type": "real",
4268+ "primaryKey": false,
4269+ "notNull": false
4270+ }
4271+ },
4272+ "indexes": {
4273+ "trial_conditions_uq": {
4274+ "name": "trial_conditions_uq",
4275+ "columns": [
4276+ {
4277+ "expression": "trial_id",
4278+ "isExpression": false,
4279+ "asc": true,
4280+ "nulls": "last"
4281+ },
4282+ {
4283+ "expression": "normalized",
4284+ "isExpression": false,
4285+ "asc": true,
4286+ "nulls": "last"
4287+ }
4288+ ],
4289+ "isUnique": true,
4290+ "concurrently": false,
4291+ "method": "btree",
4292+ "with": {}
4293+ },
4294+ "trial_conditions_cancer_idx": {
4295+ "name": "trial_conditions_cancer_idx",
4296+ "columns": [
4297+ {
4298+ "expression": "cancer_id",
4299+ "isExpression": false,
4300+ "asc": true,
4301+ "nulls": "last"
4302+ }
4303+ ],
4304+ "isUnique": false,
4305+ "concurrently": false,
4306+ "method": "btree",
4307+ "with": {}
4308+ },
4309+ "trial_conditions_norm_idx": {
4310+ "name": "trial_conditions_norm_idx",
4311+ "columns": [
4312+ {
4313+ "expression": "normalized",
4314+ "isExpression": false,
4315+ "asc": true,
4316+ "nulls": "last"
4317+ }
4318+ ],
4319+ "isUnique": false,
4320+ "concurrently": false,
4321+ "method": "btree",
4322+ "with": {}
4323+ }
4324+ },
4325+ "foreignKeys": {},
4326+ "compositePrimaryKeys": {},
4327+ "uniqueConstraints": {},
4328+ "policies": {},
4329+ "checkConstraints": {},
4330+ "isRLSEnabled": false
4331+ },
4332+ "public.trial_interventions": {
4333+ "name": "trial_interventions",
4334+ "schema": "",
4335+ "columns": {
4336+ "id": {
4337+ "name": "id",
4338+ "type": "bigserial",
4339+ "primaryKey": true,
4340+ "notNull": true
4341+ },
4342+ "trial_id": {
4343+ "name": "trial_id",
4344+ "type": "varchar(32)",
4345+ "primaryKey": false,
4346+ "notNull": true
4347+ },
4348+ "name": {
4349+ "name": "name",
4350+ "type": "text",
4351+ "primaryKey": false,
4352+ "notNull": true
4353+ },
4354+ "normalized": {
4355+ "name": "normalized",
4356+ "type": "text",
4357+ "primaryKey": false,
4358+ "notNull": true
4359+ },
4360+ "intervention_type": {
4361+ "name": "intervention_type",
4362+ "type": "text",
4363+ "primaryKey": false,
4364+ "notNull": false
4365+ },
4366+ "drug_id": {
4367+ "name": "drug_id",
4368+ "type": "varchar(32)",
4369+ "primaryKey": false,
4370+ "notNull": false
4371+ },
4372+ "match_type": {
4373+ "name": "match_type",
4374+ "type": "text",
4375+ "primaryKey": false,
4376+ "notNull": true,
4377+ "default": "'UNRESOLVED'"
4378+ }
4379+ },
4380+ "indexes": {
4381+ "trial_interventions_uq": {
4382+ "name": "trial_interventions_uq",
4383+ "columns": [
4384+ {
4385+ "expression": "trial_id",
4386+ "isExpression": false,
4387+ "asc": true,
4388+ "nulls": "last"
4389+ },
4390+ {
4391+ "expression": "normalized",
4392+ "isExpression": false,
4393+ "asc": true,
4394+ "nulls": "last"
4395+ }
4396+ ],
4397+ "isUnique": true,
4398+ "concurrently": false,
4399+ "method": "btree",
4400+ "with": {}
4401+ },
4402+ "trial_interventions_drug_idx": {
4403+ "name": "trial_interventions_drug_idx",
4404+ "columns": [
4405+ {
4406+ "expression": "drug_id",
4407+ "isExpression": false,
4408+ "asc": true,
4409+ "nulls": "last"
4410+ }
4411+ ],
4412+ "isUnique": false,
4413+ "concurrently": false,
4414+ "method": "btree",
4415+ "with": {}
4416+ }
4417+ },
4418+ "foreignKeys": {},
4419+ "compositePrimaryKeys": {},
4420+ "uniqueConstraints": {},
4421+ "policies": {},
4422+ "checkConstraints": {},
4423+ "isRLSEnabled": false
4424+ },
4425+ "public.trial_locations": {
4426+ "name": "trial_locations",
4427+ "schema": "",
4428+ "columns": {
4429+ "id": {
4430+ "name": "id",
4431+ "type": "bigserial",
4432+ "primaryKey": true,
4433+ "notNull": true
4434+ },
4435+ "trial_id": {
4436+ "name": "trial_id",
4437+ "type": "varchar(32)",
4438+ "primaryKey": false,
4439+ "notNull": true
4440+ },
4441+ "facility": {
4442+ "name": "facility",
4443+ "type": "text",
4444+ "primaryKey": false,
4445+ "notNull": false
4446+ },
4447+ "city": {
4448+ "name": "city",
4449+ "type": "text",
4450+ "primaryKey": false,
4451+ "notNull": false
4452+ },
4453+ "state": {
4454+ "name": "state",
4455+ "type": "text",
4456+ "primaryKey": false,
4457+ "notNull": false
4458+ },
4459+ "zip": {
4460+ "name": "zip",
4461+ "type": "text",
4462+ "primaryKey": false,
4463+ "notNull": false
4464+ },
4465+ "country": {
4466+ "name": "country",
4467+ "type": "text",
4468+ "primaryKey": false,
4469+ "notNull": false
4470+ },
4471+ "status": {
4472+ "name": "status",
4473+ "type": "text",
4474+ "primaryKey": false,
4475+ "notNull": false
4476+ },
4477+ "lat": {
4478+ "name": "lat",
4479+ "type": "real",
4480+ "primaryKey": false,
4481+ "notNull": false
4482+ },
4483+ "lng": {
4484+ "name": "lng",
4485+ "type": "real",
4486+ "primaryKey": false,
4487+ "notNull": false
4488+ }
4489+ },
4490+ "indexes": {
4491+ "trial_locations_trial_idx": {
4492+ "name": "trial_locations_trial_idx",
4493+ "columns": [
4494+ {
4495+ "expression": "trial_id",
4496+ "isExpression": false,
4497+ "asc": true,
4498+ "nulls": "last"
4499+ }
4500+ ],
4501+ "isUnique": false,
4502+ "concurrently": false,
4503+ "method": "btree",
4504+ "with": {}
4505+ },
4506+ "trial_locations_country_idx": {
4507+ "name": "trial_locations_country_idx",
4508+ "columns": [
4509+ {
4510+ "expression": "country",
4511+ "isExpression": false,
4512+ "asc": true,
4513+ "nulls": "last"
4514+ }
4515+ ],
4516+ "isUnique": false,
4517+ "concurrently": false,
4518+ "method": "btree",
4519+ "with": {}
4520+ }
4521+ },
4522+ "foreignKeys": {},
4523+ "compositePrimaryKeys": {},
4524+ "uniqueConstraints": {},
4525+ "policies": {},
4526+ "checkConstraints": {},
4527+ "isRLSEnabled": false
4528+ },
4529+ "public.trial_pulse": {
4530+ "name": "trial_pulse",
4531+ "schema": "",
4532+ "columns": {
4533+ "id": {
4534+ "name": "id",
4535+ "type": "bigserial",
4536+ "primaryKey": true,
4537+ "notNull": true
4538+ },
4539+ "day": {
4540+ "name": "day",
4541+ "type": "date",
4542+ "primaryKey": false,
4543+ "notNull": true
4544+ },
4545+ "cancer_id": {
4546+ "name": "cancer_id",
4547+ "type": "varchar(32)",
4548+ "primaryKey": false,
4549+ "notNull": false
4550+ },
4551+ "phase": {
4552+ "name": "phase",
4553+ "type": "text",
4554+ "primaryKey": false,
4555+ "notNull": false
4556+ },
4557+ "new_trials": {
4558+ "name": "new_trials",
4559+ "type": "integer",
4560+ "primaryKey": false,
4561+ "notNull": true
4562+ },
4563+ "updated_at": {
4564+ "name": "updated_at",
4565+ "type": "timestamp with time zone",
4566+ "primaryKey": false,
4567+ "notNull": true,
4568+ "default": "now()"
4569+ }
4570+ },
4571+ "indexes": {
4572+ "trial_pulse_uq": {
4573+ "name": "trial_pulse_uq",
4574+ "columns": [
4575+ {
4576+ "expression": "day",
4577+ "isExpression": false,
4578+ "asc": true,
4579+ "nulls": "last"
4580+ },
4581+ {
4582+ "expression": "cancer_id",
4583+ "isExpression": false,
4584+ "asc": true,
4585+ "nulls": "last"
4586+ },
4587+ {
4588+ "expression": "phase",
4589+ "isExpression": false,
4590+ "asc": true,
4591+ "nulls": "last"
4592+ }
4593+ ],
4594+ "isUnique": true,
4595+ "concurrently": false,
4596+ "method": "btree",
4597+ "with": {}
4598+ }
4599+ },
4600+ "foreignKeys": {},
4601+ "compositePrimaryKeys": {},
4602+ "uniqueConstraints": {},
4603+ "policies": {},
4604+ "checkConstraints": {},
4605+ "isRLSEnabled": false
4606+ },
4607+ "public.literature_counts": {
4608+ "name": "literature_counts",
4609+ "schema": "",
4610+ "columns": {
4611+ "id": {
4612+ "name": "id",
4613+ "type": "bigserial",
4614+ "primaryKey": true,
4615+ "notNull": true
4616+ },
4617+ "cancer_id": {
4618+ "name": "cancer_id",
4619+ "type": "varchar(32)",
4620+ "primaryKey": false,
4621+ "notNull": true
4622+ },
4623+ "window_key": {
4624+ "name": "window_key",
4625+ "type": "text",
4626+ "primaryKey": false,
4627+ "notNull": true
4628+ },
4629+ "window_start": {
4630+ "name": "window_start",
4631+ "type": "text",
4632+ "primaryKey": false,
4633+ "notNull": false
4634+ },
4635+ "window_end": {
4636+ "name": "window_end",
4637+ "type": "text",
4638+ "primaryKey": false,
4639+ "notNull": false
4640+ },
4641+ "query": {
4642+ "name": "query",
4643+ "type": "text",
4644+ "primaryKey": false,
4645+ "notNull": true
4646+ },
4647+ "count": {
4648+ "name": "count",
4649+ "type": "integer",
4650+ "primaryKey": false,
4651+ "notNull": true
4652+ },
4653+ "provenance_id": {
4654+ "name": "provenance_id",
4655+ "type": "integer",
4656+ "primaryKey": false,
4657+ "notNull": true
4658+ },
4659+ "updated_at": {
4660+ "name": "updated_at",
4661+ "type": "timestamp with time zone",
4662+ "primaryKey": false,
4663+ "notNull": true,
4664+ "default": "now()"
4665+ }
4666+ },
4667+ "indexes": {
4668+ "literature_counts_uq": {
4669+ "name": "literature_counts_uq",
4670+ "columns": [
4671+ {
4672+ "expression": "cancer_id",
4673+ "isExpression": false,
4674+ "asc": true,
4675+ "nulls": "last"
4676+ },
4677+ {
4678+ "expression": "window_key",
4679+ "isExpression": false,
4680+ "asc": true,
4681+ "nulls": "last"
4682+ }
4683+ ],
4684+ "isUnique": true,
4685+ "concurrently": false,
4686+ "method": "btree",
4687+ "with": {}
4688+ },
4689+ "literature_counts_window_idx": {
4690+ "name": "literature_counts_window_idx",
4691+ "columns": [
4692+ {
4693+ "expression": "window_key",
4694+ "isExpression": false,
4695+ "asc": true,
4696+ "nulls": "last"
4697+ },
4698+ {
4699+ "expression": "count",
4700+ "isExpression": false,
4701+ "asc": true,
4702+ "nulls": "last"
4703+ }
4704+ ],
4705+ "isUnique": false,
4706+ "concurrently": false,
4707+ "method": "btree",
4708+ "with": {}
4709+ }
4710+ },
4711+ "foreignKeys": {},
4712+ "compositePrimaryKeys": {},
4713+ "uniqueConstraints": {},
4714+ "policies": {},
4715+ "checkConstraints": {},
4716+ "isRLSEnabled": false
4717+ },
4718+ "public.publication_entity_edges": {
4719+ "name": "publication_entity_edges",
4720+ "schema": "",
4721+ "columns": {
4722+ "id": {
4723+ "name": "id",
4724+ "type": "bigserial",
4725+ "primaryKey": true,
4726+ "notNull": true
4727+ },
4728+ "publication_id": {
4729+ "name": "publication_id",
4730+ "type": "varchar(32)",
4731+ "primaryKey": false,
4732+ "notNull": true
4733+ },
4734+ "entity_type": {
4735+ "name": "entity_type",
4736+ "type": "text",
4737+ "primaryKey": false,
4738+ "notNull": true
4739+ },
4740+ "entity_id": {
4741+ "name": "entity_id",
4742+ "type": "text",
4743+ "primaryKey": false,
4744+ "notNull": true
4745+ },
4746+ "method": {
4747+ "name": "method",
4748+ "type": "text",
4749+ "primaryKey": false,
4750+ "notNull": true
4751+ },
4752+ "confidence": {
4753+ "name": "confidence",
4754+ "type": "real",
4755+ "primaryKey": false,
4756+ "notNull": false
4757+ },
4758+ "status": {
4759+ "name": "status",
4760+ "type": "text",
4761+ "primaryKey": false,
4762+ "notNull": true,
4763+ "default": "'candidate'"
4764+ },
4765+ "source_id": {
4766+ "name": "source_id",
4767+ "type": "varchar(32)",
4768+ "primaryKey": false,
4769+ "notNull": false
4770+ },
4771+ "ingest_run_id": {
4772+ "name": "ingest_run_id",
4773+ "type": "text",
4774+ "primaryKey": false,
4775+ "notNull": false
4776+ },
4777+ "created_at": {
4778+ "name": "created_at",
4779+ "type": "timestamp with time zone",
4780+ "primaryKey": false,
4781+ "notNull": true,
4782+ "default": "now()"
4783+ }
4784+ },
4785+ "indexes": {
4786+ "pub_entity_edges_uq": {
4787+ "name": "pub_entity_edges_uq",
4788+ "columns": [
4789+ {
4790+ "expression": "publication_id",
4791+ "isExpression": false,
4792+ "asc": true,
4793+ "nulls": "last"
4794+ },
4795+ {
4796+ "expression": "entity_type",
4797+ "isExpression": false,
4798+ "asc": true,
4799+ "nulls": "last"
4800+ },
4801+ {
4802+ "expression": "entity_id",
4803+ "isExpression": false,
4804+ "asc": true,
4805+ "nulls": "last"
4806+ },
4807+ {
4808+ "expression": "method",
4809+ "isExpression": false,
4810+ "asc": true,
4811+ "nulls": "last"
4812+ }
4813+ ],
4814+ "isUnique": true,
4815+ "concurrently": false,
4816+ "method": "btree",
4817+ "with": {}
4818+ },
4819+ "pub_entity_edges_entity_idx": {
4820+ "name": "pub_entity_edges_entity_idx",
4821+ "columns": [
4822+ {
4823+ "expression": "entity_type",
4824+ "isExpression": false,
4825+ "asc": true,
4826+ "nulls": "last"
4827+ },
4828+ {
4829+ "expression": "entity_id",
4830+ "isExpression": false,
4831+ "asc": true,
4832+ "nulls": "last"
4833+ }
4834+ ],
4835+ "isUnique": false,
4836+ "concurrently": false,
4837+ "method": "btree",
4838+ "with": {}
4839+ }
4840+ },
4841+ "foreignKeys": {},
4842+ "compositePrimaryKeys": {},
4843+ "uniqueConstraints": {},
4844+ "policies": {},
4845+ "checkConstraints": {},
4846+ "isRLSEnabled": false
4847+ },
4848+ "public.publications": {
4849+ "name": "publications",
4850+ "schema": "",
4851+ "columns": {
4852+ "id": {
4853+ "name": "id",
4854+ "type": "varchar(32)",
4855+ "primaryKey": true,
4856+ "notNull": true
4857+ },
4858+ "pmid": {
4859+ "name": "pmid",
4860+ "type": "text",
4861+ "primaryKey": false,
4862+ "notNull": false
4863+ },
4864+ "doi": {
4865+ "name": "doi",
4866+ "type": "text",
4867+ "primaryKey": false,
4868+ "notNull": false
4869+ },
4870+ "pmcid": {
4871+ "name": "pmcid",
4872+ "type": "text",
4873+ "primaryKey": false,
4874+ "notNull": false
4875+ },
4876+ "title": {
4877+ "name": "title",
4878+ "type": "text",
4879+ "primaryKey": false,
4880+ "notNull": true
4881+ },
4882+ "abstract": {
4883+ "name": "abstract",
4884+ "type": "text",
4885+ "primaryKey": false,
4886+ "notNull": false
4887+ },
4888+ "journal": {
4889+ "name": "journal",
4890+ "type": "text",
4891+ "primaryKey": false,
4892+ "notNull": false
4893+ },
4894+ "journal_iso": {
4895+ "name": "journal_iso",
4896+ "type": "text",
4897+ "primaryKey": false,
4898+ "notNull": false
4899+ },
4900+ "pub_date": {
4901+ "name": "pub_date",
4902+ "type": "text",
4903+ "primaryKey": false,
4904+ "notNull": false
4905+ },
4906+ "pub_year": {
4907+ "name": "pub_year",
4908+ "type": "integer",
4909+ "primaryKey": false,
4910+ "notNull": false
4911+ },
4912+ "publication_types": {
4913+ "name": "publication_types",
4914+ "type": "text[]",
4915+ "primaryKey": false,
4916+ "notNull": true,
4917+ "default": "'{}'"
4918+ },
4919+ "mesh_terms": {
4920+ "name": "mesh_terms",
4921+ "type": "jsonb",
4922+ "primaryKey": false,
4923+ "notNull": true,
4924+ "default": "'[]'::jsonb"
4925+ },
4926+ "authors": {
4927+ "name": "authors",
4928+ "type": "jsonb",
4929+ "primaryKey": false,
4930+ "notNull": true,
4931+ "default": "'[]'::jsonb"
4932+ },
4933+ "language": {
4934+ "name": "language",
4935+ "type": "text",
4936+ "primaryKey": false,
4937+ "notNull": false
4938+ },
4939+ "is_preprint": {
4940+ "name": "is_preprint",
4941+ "type": "boolean",
4942+ "primaryKey": false,
4943+ "notNull": true,
4944+ "default": false
4945+ },
4946+ "retracted": {
4947+ "name": "retracted",
4948+ "type": "boolean",
4949+ "primaryKey": false,
4950+ "notNull": true,
4951+ "default": false
4952+ },
4953+ "retraction_notice": {
4954+ "name": "retraction_notice",
4955+ "type": "text",
4956+ "primaryKey": false,
4957+ "notNull": false
4958+ },
4959+ "nct_ids": {
4960+ "name": "nct_ids",
4961+ "type": "text[]",
4962+ "primaryKey": false,
4963+ "notNull": true,
4964+ "default": "'{}'"
4965+ },
4966+ "cited_by_count": {
4967+ "name": "cited_by_count",
4968+ "type": "integer",
4969+ "primaryKey": false,
4970+ "notNull": false
4971+ },
4972+ "source_record_id": {
4973+ "name": "source_record_id",
4974+ "type": "integer",
4975+ "primaryKey": false,
4976+ "notNull": false
4977+ },
4978+ "ingest_run_id": {
4979+ "name": "ingest_run_id",
4980+ "type": "text",
4981+ "primaryKey": false,
4982+ "notNull": false
4983+ },
4984+ "created_at": {
4985+ "name": "created_at",
4986+ "type": "timestamp with time zone",
4987+ "primaryKey": false,
4988+ "notNull": true,
4989+ "default": "now()"
4990+ },
4991+ "updated_at": {
4992+ "name": "updated_at",
4993+ "type": "timestamp with time zone",
4994+ "primaryKey": false,
4995+ "notNull": true,
4996+ "default": "now()"
4997+ }
4998+ },
4999+ "indexes": {
5000+ "publications_pmid_uq": {
5001+ "name": "publications_pmid_uq",
5002+ "columns": [
5003+ {
5004+ "expression": "pmid",
5005+ "isExpression": false,
5006+ "asc": true,
5007+ "nulls": "last"
5008+ }
5009+ ],
5010+ "isUnique": true,
5011+ "concurrently": false,
5012+ "method": "btree",
5013+ "with": {}
5014+ },
5015+ "publications_doi_idx": {
5016+ "name": "publications_doi_idx",
5017+ "columns": [
5018+ {
5019+ "expression": "doi",
5020+ "isExpression": false,
5021+ "asc": true,
5022+ "nulls": "last"
5023+ }
5024+ ],
5025+ "isUnique": false,
5026+ "concurrently": false,
5027+ "method": "btree",
5028+ "with": {}
5029+ },
5030+ "publications_year_idx": {
5031+ "name": "publications_year_idx",
5032+ "columns": [
5033+ {
5034+ "expression": "pub_year",
5035+ "isExpression": false,
5036+ "asc": true,
5037+ "nulls": "last"
5038+ }
5039+ ],
5040+ "isUnique": false,
5041+ "concurrently": false,
5042+ "method": "btree",
5043+ "with": {}
5044+ },
5045+ "publications_retracted_idx": {
5046+ "name": "publications_retracted_idx",
5047+ "columns": [
5048+ {
5049+ "expression": "retracted",
5050+ "isExpression": false,
5051+ "asc": true,
5052+ "nulls": "last"
5053+ }
5054+ ],
5055+ "isUnique": false,
5056+ "concurrently": false,
5057+ "method": "btree",
5058+ "with": {}
5059+ }
5060+ },
5061+ "foreignKeys": {},
5062+ "compositePrimaryKeys": {},
5063+ "uniqueConstraints": {},
5064+ "policies": {},
5065+ "checkConstraints": {},
5066+ "isRLSEnabled": false
5067+ },
5068+ "public.civic_evidence_items": {
5069+ "name": "civic_evidence_items",
5070+ "schema": "",
5071+ "columns": {
5072+ "id": {
5073+ "name": "id",
5074+ "type": "bigserial",
5075+ "primaryKey": true,
5076+ "notNull": true
5077+ },
5078+ "civic_id": {
5079+ "name": "civic_id",
5080+ "type": "integer",
5081+ "primaryKey": false,
5082+ "notNull": true
5083+ },
5084+ "name": {
5085+ "name": "name",
5086+ "type": "text",
5087+ "primaryKey": false,
5088+ "notNull": false
5089+ },
5090+ "molecular_profile_id": {
5091+ "name": "molecular_profile_id",
5092+ "type": "integer",
5093+ "primaryKey": false,
5094+ "notNull": false
5095+ },
5096+ "molecular_profile_name": {
5097+ "name": "molecular_profile_name",
5098+ "type": "text",
5099+ "primaryKey": false,
5100+ "notNull": false
5101+ },
5102+ "gene_symbols": {
5103+ "name": "gene_symbols",
5104+ "type": "text[]",
5105+ "primaryKey": false,
5106+ "notNull": true,
5107+ "default": "'{}'"
5108+ },
5109+ "gene_ids": {
5110+ "name": "gene_ids",
5111+ "type": "text[]",
5112+ "primaryKey": false,
5113+ "notNull": true,
5114+ "default": "'{}'"
5115+ },
5116+ "variant_ids": {
5117+ "name": "variant_ids",
5118+ "type": "text[]",
5119+ "primaryKey": false,
5120+ "notNull": true,
5121+ "default": "'{}'"
5122+ },
5123+ "civic_variant_ids": {
5124+ "name": "civic_variant_ids",
5125+ "type": "integer[]",
5126+ "primaryKey": false,
5127+ "notNull": true,
5128+ "default": "'{}'"
5129+ },
5130+ "disease_name": {
5131+ "name": "disease_name",
5132+ "type": "text",
5133+ "primaryKey": false,
5134+ "notNull": false
5135+ },
5136+ "doid": {
5137+ "name": "doid",
5138+ "type": "text",
5139+ "primaryKey": false,
5140+ "notNull": false
5141+ },
5142+ "cancer_id": {
5143+ "name": "cancer_id",
5144+ "type": "varchar(32)",
5145+ "primaryKey": false,
5146+ "notNull": false
5147+ },
5148+ "cancer_match_type": {
5149+ "name": "cancer_match_type",
5150+ "type": "text",
5151+ "primaryKey": false,
5152+ "notNull": false
5153+ },
5154+ "therapy_names": {
5155+ "name": "therapy_names",
5156+ "type": "text[]",
5157+ "primaryKey": false,
5158+ "notNull": true,
5159+ "default": "'{}'"
5160+ },
5161+ "therapy_ids": {
5162+ "name": "therapy_ids",
5163+ "type": "text[]",
5164+ "primaryKey": false,
5165+ "notNull": true,
5166+ "default": "'{}'"
5167+ },
5168+ "therapy_interaction_type": {
5169+ "name": "therapy_interaction_type",
5170+ "type": "text",
5171+ "primaryKey": false,
5172+ "notNull": false
5173+ },
5174+ "evidence_type": {
5175+ "name": "evidence_type",
5176+ "type": "text",
5177+ "primaryKey": false,
5178+ "notNull": false
5179+ },
5180+ "evidence_level": {
5181+ "name": "evidence_level",
5182+ "type": "text",
5183+ "primaryKey": false,
5184+ "notNull": false
5185+ },
5186+ "evidence_direction": {
5187+ "name": "evidence_direction",
5188+ "type": "text",
5189+ "primaryKey": false,
5190+ "notNull": false
5191+ },
5192+ "significance": {
5193+ "name": "significance",
5194+ "type": "text",
5195+ "primaryKey": false,
5196+ "notNull": false
5197+ },
5198+ "evidence_rating": {
5199+ "name": "evidence_rating",
5200+ "type": "integer",
5201+ "primaryKey": false,
5202+ "notNull": false
5203+ },
5204+ "status": {
5205+ "name": "status",
5206+ "type": "text",
5207+ "primaryKey": false,
5208+ "notNull": false
5209+ },
5210+ "description": {
5211+ "name": "description",
5212+ "type": "text",
5213+ "primaryKey": false,
5214+ "notNull": false
5215+ },
5216+ "pmid": {
5217+ "name": "pmid",
5218+ "type": "text",
5219+ "primaryKey": false,
5220+ "notNull": false
5221+ },
5222+ "source_citation": {
5223+ "name": "source_citation",
5224+ "type": "text",
5225+ "primaryKey": false,
5226+ "notNull": false
5227+ },
5228+ "phenotypes": {
5229+ "name": "phenotypes",
5230+ "type": "text[]",
5231+ "primaryKey": false,
5232+ "notNull": true,
5233+ "default": "'{}'"
5234+ },
5235+ "provenance_id": {
5236+ "name": "provenance_id",
5237+ "type": "integer",
5238+ "primaryKey": false,
5239+ "notNull": true
5240+ },
5241+ "ingest_run_id": {
5242+ "name": "ingest_run_id",
5243+ "type": "text",
5244+ "primaryKey": false,
5245+ "notNull": false
5246+ },
5247+ "updated_at": {
5248+ "name": "updated_at",
5249+ "type": "timestamp with time zone",
5250+ "primaryKey": false,
5251+ "notNull": true,
5252+ "default": "now()"
5253+ }
5254+ },
5255+ "indexes": {
5256+ "civic_evidence_uq": {
5257+ "name": "civic_evidence_uq",
5258+ "columns": [
5259+ {
5260+ "expression": "civic_id",
5261+ "isExpression": false,
5262+ "asc": true,
5263+ "nulls": "last"
5264+ }
5265+ ],
5266+ "isUnique": true,
5267+ "concurrently": false,
5268+ "method": "btree",
5269+ "with": {}
5270+ },
5271+ "civic_evidence_cancer_idx": {
5272+ "name": "civic_evidence_cancer_idx",
5273+ "columns": [
5274+ {
5275+ "expression": "cancer_id",
5276+ "isExpression": false,
5277+ "asc": true,
5278+ "nulls": "last"
5279+ }
5280+ ],
5281+ "isUnique": false,
5282+ "concurrently": false,
5283+ "method": "btree",
5284+ "with": {}
5285+ },
5286+ "civic_evidence_gene_idx": {
5287+ "name": "civic_evidence_gene_idx",
5288+ "columns": [
5289+ {
5290+ "expression": "gene_symbols",
5291+ "isExpression": false,
5292+ "asc": true,
5293+ "nulls": "last"
5294+ }
5295+ ],
5296+ "isUnique": false,
5297+ "concurrently": false,
5298+ "method": "btree",
5299+ "with": {}
5300+ }
5301+ },
5302+ "foreignKeys": {},
5303+ "compositePrimaryKeys": {},
5304+ "uniqueConstraints": {},
5305+ "policies": {},
5306+ "checkConstraints": {},
5307+ "isRLSEnabled": false
5308+ },
5309+ "public.knowledge_edges": {
5310+ "name": "knowledge_edges",
5311+ "schema": "",
5312+ "columns": {
5313+ "id": {
5314+ "name": "id",
5315+ "type": "bigserial",
5316+ "primaryKey": true,
5317+ "notNull": true
5318+ },
5319+ "source_entity_type": {
5320+ "name": "source_entity_type",
5321+ "type": "text",
5322+ "primaryKey": false,
5323+ "notNull": true
5324+ },
5325+ "source_entity_id": {
5326+ "name": "source_entity_id",
5327+ "type": "text",
5328+ "primaryKey": false,
5329+ "notNull": true
5330+ },
5331+ "target_entity_type": {
5332+ "name": "target_entity_type",
5333+ "type": "text",
5334+ "primaryKey": false,
5335+ "notNull": true
5336+ },
5337+ "target_entity_id": {
5338+ "name": "target_entity_id",
5339+ "type": "text",
5340+ "primaryKey": false,
5341+ "notNull": true
5342+ },
5343+ "relationship_type": {
5344+ "name": "relationship_type",
5345+ "type": "text",
5346+ "primaryKey": false,
5347+ "notNull": true
5348+ },
5349+ "cancer_context_ids": {
5350+ "name": "cancer_context_ids",
5351+ "type": "text[]",
5352+ "primaryKey": false,
5353+ "notNull": true,
5354+ "default": "'{}'"
5355+ },
5356+ "predictive": {
5357+ "name": "predictive",
5358+ "type": "boolean",
5359+ "primaryKey": false,
5360+ "notNull": false
5361+ },
5362+ "prognostic": {
5363+ "name": "prognostic",
5364+ "type": "boolean",
5365+ "primaryKey": false,
5366+ "notNull": false
5367+ },
5368+ "diagnostic": {
5369+ "name": "diagnostic",
5370+ "type": "boolean",
5371+ "primaryKey": false,
5372+ "notNull": false
5373+ },
5374+ "predisposing": {
5375+ "name": "predisposing",
5376+ "type": "boolean",
5377+ "primaryKey": false,
5378+ "notNull": false
5379+ },
5380+ "direction": {
5381+ "name": "direction",
5382+ "type": "text",
5383+ "primaryKey": false,
5384+ "notNull": false
5385+ },
5386+ "evidence_level": {
5387+ "name": "evidence_level",
5388+ "type": "text",
5389+ "primaryKey": false,
5390+ "notNull": false
5391+ },
5392+ "evidence_score": {
5393+ "name": "evidence_score",
5394+ "type": "real",
5395+ "primaryKey": false,
5396+ "notNull": false
5397+ },
5398+ "evidence_category": {
5399+ "name": "evidence_category",
5400+ "type": "text",
5401+ "primaryKey": false,
5402+ "notNull": true,
5403+ "default": "'curated_evidence'"
5404+ },
5405+ "status": {
5406+ "name": "status",
5407+ "type": "text",
5408+ "primaryKey": false,
5409+ "notNull": true,
5410+ "default": "'active'"
5411+ },
5412+ "source_id": {
5413+ "name": "source_id",
5414+ "type": "varchar(32)",
5415+ "primaryKey": false,
5416+ "notNull": true
5417+ },
5418+ "source_record_id": {
5419+ "name": "source_record_id",
5420+ "type": "text",
5421+ "primaryKey": false,
5422+ "notNull": false
5423+ },
5424+ "provenance_ids": {
5425+ "name": "provenance_ids",
5426+ "type": "integer[]",
5427+ "primaryKey": false,
5428+ "notNull": true,
5429+ "default": "'{}'"
5430+ },
5431+ "support_count": {
5432+ "name": "support_count",
5433+ "type": "integer",
5434+ "primaryKey": false,
5435+ "notNull": true,
5436+ "default": 1
5437+ },
5438+ "first_seen_at": {
5439+ "name": "first_seen_at",
5440+ "type": "timestamp with time zone",
5441+ "primaryKey": false,
5442+ "notNull": true,
5443+ "default": "now()"
5444+ },
5445+ "last_seen_at": {
5446+ "name": "last_seen_at",
5447+ "type": "timestamp with time zone",
5448+ "primaryKey": false,
5449+ "notNull": true,
5450+ "default": "now()"
5451+ }
5452+ },
5453+ "indexes": {
5454+ "knowledge_edges_uq": {
5455+ "name": "knowledge_edges_uq",
5456+ "columns": [
5457+ {
5458+ "expression": "source_entity_type",
5459+ "isExpression": false,
5460+ "asc": true,
5461+ "nulls": "last"
5462+ },
5463+ {
5464+ "expression": "source_entity_id",
5465+ "isExpression": false,
5466+ "asc": true,
5467+ "nulls": "last"
5468+ },
5469+ {
5470+ "expression": "target_entity_type",
5471+ "isExpression": false,
5472+ "asc": true,
5473+ "nulls": "last"
5474+ },
5475+ {
5476+ "expression": "target_entity_id",
5477+ "isExpression": false,
5478+ "asc": true,
5479+ "nulls": "last"
5480+ },
5481+ {
5482+ "expression": "relationship_type",
5483+ "isExpression": false,
5484+ "asc": true,
5485+ "nulls": "last"
5486+ },
5487+ {
5488+ "expression": "source_id",
5489+ "isExpression": false,
5490+ "asc": true,
5491+ "nulls": "last"
5492+ },
5493+ {
5494+ "expression": "source_record_id",
5495+ "isExpression": false,
5496+ "asc": true,
5497+ "nulls": "last"
5498+ }
5499+ ],
5500+ "isUnique": true,
5501+ "concurrently": false,
5502+ "method": "btree",
5503+ "with": {}
5504+ },
5505+ "knowledge_edges_source_idx": {
5506+ "name": "knowledge_edges_source_idx",
5507+ "columns": [
5508+ {
5509+ "expression": "source_entity_type",
5510+ "isExpression": false,
5511+ "asc": true,
5512+ "nulls": "last"
5513+ },
5514+ {
5515+ "expression": "source_entity_id",
5516+ "isExpression": false,
5517+ "asc": true,
5518+ "nulls": "last"
5519+ },
5520+ {
5521+ "expression": "relationship_type",
5522+ "isExpression": false,
5523+ "asc": true,
5524+ "nulls": "last"
5525+ }
5526+ ],
5527+ "isUnique": false,
5528+ "concurrently": false,
5529+ "method": "btree",
5530+ "with": {}
5531+ },
5532+ "knowledge_edges_target_idx": {
5533+ "name": "knowledge_edges_target_idx",
5534+ "columns": [
5535+ {
5536+ "expression": "target_entity_type",
5537+ "isExpression": false,
5538+ "asc": true,
5539+ "nulls": "last"
5540+ },
5541+ {
5542+ "expression": "target_entity_id",
5543+ "isExpression": false,
5544+ "asc": true,
5545+ "nulls": "last"
5546+ },
5547+ {
5548+ "expression": "relationship_type",
5549+ "isExpression": false,
5550+ "asc": true,
5551+ "nulls": "last"
5552+ }
5553+ ],
5554+ "isUnique": false,
5555+ "concurrently": false,
5556+ "method": "btree",
5557+ "with": {}
5558+ }
5559+ },
5560+ "foreignKeys": {},
5561+ "compositePrimaryKeys": {},
5562+ "uniqueConstraints": {},
5563+ "policies": {},
5564+ "checkConstraints": {},
5565+ "isRLSEnabled": false
5566+ },
5567+ "public.risk_factors": {
5568+ "name": "risk_factors",
5569+ "schema": "",
5570+ "columns": {
5571+ "id": {
5572+ "name": "id",
5573+ "type": "bigserial",
5574+ "primaryKey": true,
5575+ "notNull": true
5576+ },
5577+ "slug": {
5578+ "name": "slug",
5579+ "type": "text",
5580+ "primaryKey": false,
5581+ "notNull": true
5582+ },
5583+ "name": {
5584+ "name": "name",
5585+ "type": "text",
5586+ "primaryKey": false,
5587+ "notNull": true
5588+ },
5589+ "kind": {
5590+ "name": "kind",
5591+ "type": "text",
5592+ "primaryKey": false,
5593+ "notNull": true
5594+ },
5595+ "classification_authority": {
5596+ "name": "classification_authority",
5597+ "type": "text",
5598+ "primaryKey": false,
5599+ "notNull": false
5600+ },
5601+ "classification": {
5602+ "name": "classification",
5603+ "type": "text",
5604+ "primaryKey": false,
5605+ "notNull": false
5606+ },
5607+ "description": {
5608+ "name": "description",
5609+ "type": "text",
5610+ "primaryKey": false,
5611+ "notNull": false
5612+ },
5613+ "created_at": {
5614+ "name": "created_at",
5615+ "type": "timestamp with time zone",
5616+ "primaryKey": false,
5617+ "notNull": true,
5618+ "default": "now()"
5619+ }
5620+ },
5621+ "indexes": {
5622+ "risk_factors_slug_uq": {
5623+ "name": "risk_factors_slug_uq",
5624+ "columns": [
5625+ {
5626+ "expression": "slug",
5627+ "isExpression": false,
5628+ "asc": true,
5629+ "nulls": "last"
5630+ }
5631+ ],
5632+ "isUnique": true,
5633+ "concurrently": false,
5634+ "method": "btree",
5635+ "with": {}
5636+ }
5637+ },
5638+ "foreignKeys": {},
5639+ "compositePrimaryKeys": {},
5640+ "uniqueConstraints": {},
5641+ "policies": {},
5642+ "checkConstraints": {},
5643+ "isRLSEnabled": false
5644+ },
5645+ "public.epidemiology_observations": {
5646+ "name": "epidemiology_observations",
5647+ "schema": "",
5648+ "columns": {
5649+ "id": {
5650+ "name": "id",
5651+ "type": "bigserial",
5652+ "primaryKey": true,
5653+ "notNull": true
5654+ },
5655+ "cancer_id": {
5656+ "name": "cancer_id",
5657+ "type": "varchar(32)",
5658+ "primaryKey": false,
5659+ "notNull": true
5660+ },
5661+ "geography_id": {
5662+ "name": "geography_id",
5663+ "type": "varchar(32)",
5664+ "primaryKey": false,
5665+ "notNull": true
5666+ },
5667+ "year": {
5668+ "name": "year",
5669+ "type": "integer",
5670+ "primaryKey": false,
5671+ "notNull": true
5672+ },
5673+ "year_end": {
5674+ "name": "year_end",
5675+ "type": "integer",
5676+ "primaryKey": false,
5677+ "notNull": false
5678+ },
5679+ "sex": {
5680+ "name": "sex",
5681+ "type": "text",
5682+ "primaryKey": false,
5683+ "notNull": true,
5684+ "default": "'all'"
5685+ },
5686+ "age_group": {
5687+ "name": "age_group",
5688+ "type": "text",
5689+ "primaryKey": false,
5690+ "notNull": true,
5691+ "default": "'all'"
5692+ },
5693+ "metric": {
5694+ "name": "metric",
5695+ "type": "text",
5696+ "primaryKey": false,
5697+ "notNull": true
5698+ },
5699+ "value": {
5700+ "name": "value",
5701+ "type": "double precision",
5702+ "primaryKey": false,
5703+ "notNull": true
5704+ },
5705+ "unit": {
5706+ "name": "unit",
5707+ "type": "text",
5708+ "primaryKey": false,
5709+ "notNull": true
5710+ },
5711+ "lower_ci": {
5712+ "name": "lower_ci",
5713+ "type": "double precision",
5714+ "primaryKey": false,
5715+ "notNull": false
5716+ },
5717+ "upper_ci": {
5718+ "name": "upper_ci",
5719+ "type": "double precision",
5720+ "primaryKey": false,
5721+ "notNull": false
5722+ },
5723+ "standard_population": {
5724+ "name": "standard_population",
5725+ "type": "text",
5726+ "primaryKey": false,
5727+ "notNull": false
5728+ },
5729+ "estimate_type": {
5730+ "name": "estimate_type",
5731+ "type": "text",
5732+ "primaryKey": false,
5733+ "notNull": true,
5734+ "default": "'observed'"
5735+ },
5736+ "site_definition": {
5737+ "name": "site_definition",
5738+ "type": "text",
5739+ "primaryKey": false,
5740+ "notNull": false
5741+ },
5742+ "source_id": {
5743+ "name": "source_id",
5744+ "type": "varchar(32)",
5745+ "primaryKey": false,
5746+ "notNull": true
5747+ },
5748+ "provenance_id": {
5749+ "name": "provenance_id",
5750+ "type": "integer",
5751+ "primaryKey": false,
5752+ "notNull": true
5753+ },
5754+ "ingest_run_id": {
5755+ "name": "ingest_run_id",
5756+ "type": "text",
5757+ "primaryKey": false,
5758+ "notNull": false
5759+ },
5760+ "updated_at": {
5761+ "name": "updated_at",
5762+ "type": "timestamp with time zone",
5763+ "primaryKey": false,
5764+ "notNull": true,
5765+ "default": "now()"
5766+ }
5767+ },
5768+ "indexes": {
5769+ "epi_obs_uq": {
5770+ "name": "epi_obs_uq",
5771+ "columns": [
5772+ {
5773+ "expression": "cancer_id",
5774+ "isExpression": false,
5775+ "asc": true,
5776+ "nulls": "last"
5777+ },
5778+ {
5779+ "expression": "geography_id",
5780+ "isExpression": false,
5781+ "asc": true,
5782+ "nulls": "last"
5783+ },
5784+ {
5785+ "expression": "year",
5786+ "isExpression": false,
5787+ "asc": true,
5788+ "nulls": "last"
5789+ },
5790+ {
5791+ "expression": "sex",
5792+ "isExpression": false,
5793+ "asc": true,
5794+ "nulls": "last"
5795+ },
5796+ {
5797+ "expression": "age_group",
5798+ "isExpression": false,
5799+ "asc": true,
5800+ "nulls": "last"
5801+ },
5802+ {
5803+ "expression": "metric",
5804+ "isExpression": false,
5805+ "asc": true,
5806+ "nulls": "last"
5807+ },
5808+ {
5809+ "expression": "source_id",
5810+ "isExpression": false,
5811+ "asc": true,
5812+ "nulls": "last"
5813+ },
5814+ {
5815+ "expression": "site_definition",
5816+ "isExpression": false,
5817+ "asc": true,
5818+ "nulls": "last"
5819+ }
5820+ ],
5821+ "isUnique": true,
5822+ "concurrently": false,
5823+ "method": "btree",
5824+ "with": {}
5825+ },
5826+ "epi_obs_lookup_idx": {
5827+ "name": "epi_obs_lookup_idx",
5828+ "columns": [
5829+ {
5830+ "expression": "metric",
5831+ "isExpression": false,
5832+ "asc": true,
5833+ "nulls": "last"
5834+ },
5835+ {
5836+ "expression": "geography_id",
5837+ "isExpression": false,
5838+ "asc": true,
5839+ "nulls": "last"
5840+ },
5841+ {
5842+ "expression": "year",
5843+ "isExpression": false,
5844+ "asc": true,
5845+ "nulls": "last"
5846+ },
5847+ {
5848+ "expression": "sex",
5849+ "isExpression": false,
5850+ "asc": true,
5851+ "nulls": "last"
5852+ }
5853+ ],
5854+ "isUnique": false,
5855+ "concurrently": false,
5856+ "method": "btree",
5857+ "with": {}
5858+ },
5859+ "epi_obs_cancer_idx": {
5860+ "name": "epi_obs_cancer_idx",
5861+ "columns": [
5862+ {
5863+ "expression": "cancer_id",
5864+ "isExpression": false,
5865+ "asc": true,
5866+ "nulls": "last"
5867+ },
5868+ {
5869+ "expression": "metric",
5870+ "isExpression": false,
5871+ "asc": true,
5872+ "nulls": "last"
5873+ }
5874+ ],
5875+ "isUnique": false,
5876+ "concurrently": false,
5877+ "method": "btree",
5878+ "with": {}
5879+ }
5880+ },
5881+ "foreignKeys": {},
5882+ "compositePrimaryKeys": {},
5883+ "uniqueConstraints": {},
5884+ "policies": {},
5885+ "checkConstraints": {},
5886+ "isRLSEnabled": false
5887+ },
5888+ "public.survival_observations": {
5889+ "name": "survival_observations",
5890+ "schema": "",
5891+ "columns": {
5892+ "id": {
5893+ "name": "id",
5894+ "type": "bigserial",
5895+ "primaryKey": true,
5896+ "notNull": true
5897+ },
5898+ "cancer_id": {
5899+ "name": "cancer_id",
5900+ "type": "varchar(32)",
5901+ "primaryKey": false,
5902+ "notNull": true
5903+ },
5904+ "geography_id": {
5905+ "name": "geography_id",
5906+ "type": "varchar(32)",
5907+ "primaryKey": false,
5908+ "notNull": false
5909+ },
5910+ "stage": {
5911+ "name": "stage",
5912+ "type": "text",
5913+ "primaryKey": false,
5914+ "notNull": false
5915+ },
5916+ "staging_system": {
5917+ "name": "staging_system",
5918+ "type": "text",
5919+ "primaryKey": false,
5920+ "notNull": false
5921+ },
5922+ "sex": {
5923+ "name": "sex",
5924+ "type": "text",
5925+ "primaryKey": false,
5926+ "notNull": true,
5927+ "default": "'all'"
5928+ },
5929+ "age_group": {
5930+ "name": "age_group",
5931+ "type": "text",
5932+ "primaryKey": false,
5933+ "notNull": true,
5934+ "default": "'all'"
5935+ },
5936+ "diagnosis_period": {
5937+ "name": "diagnosis_period",
5938+ "type": "text",
5939+ "primaryKey": false,
5940+ "notNull": false
5941+ },
5942+ "survival_type": {
5943+ "name": "survival_type",
5944+ "type": "text",
5945+ "primaryKey": false,
5946+ "notNull": true
5947+ },
5948+ "duration_months": {
5949+ "name": "duration_months",
5950+ "type": "integer",
5951+ "primaryKey": false,
5952+ "notNull": true
5953+ },
5954+ "probability": {
5955+ "name": "probability",
5956+ "type": "real",
5957+ "primaryKey": false,
5958+ "notNull": false
5959+ },
5960+ "median_months": {
5961+ "name": "median_months",
5962+ "type": "real",
5963+ "primaryKey": false,
5964+ "notNull": false
5965+ },
5966+ "cohort_size": {
5967+ "name": "cohort_size",
5968+ "type": "integer",
5969+ "primaryKey": false,
5970+ "notNull": false
5971+ },
5972+ "lower_ci": {
5973+ "name": "lower_ci",
5974+ "type": "real",
5975+ "primaryKey": false,
5976+ "notNull": false
5977+ },
5978+ "upper_ci": {
5979+ "name": "upper_ci",
5980+ "type": "real",
5981+ "primaryKey": false,
5982+ "notNull": false
5983+ },
5984+ "method": {
5985+ "name": "method",
5986+ "type": "text",
5987+ "primaryKey": false,
5988+ "notNull": false
5989+ },
5990+ "source_id": {
5991+ "name": "source_id",
5992+ "type": "varchar(32)",
5993+ "primaryKey": false,
5994+ "notNull": true
5995+ },
5996+ "provenance_id": {
5997+ "name": "provenance_id",
5998+ "type": "integer",
5999+ "primaryKey": false,
6000+ "notNull": true
6001+ },
6002+ "ingest_run_id": {
6003+ "name": "ingest_run_id",
6004+ "type": "text",
6005+ "primaryKey": false,
6006+ "notNull": false
6007+ },
6008+ "updated_at": {
6009+ "name": "updated_at",
6010+ "type": "timestamp with time zone",
6011+ "primaryKey": false,
6012+ "notNull": true,
6013+ "default": "now()"
6014+ }
6015+ },
6016+ "indexes": {
6017+ "survival_obs_cancer_idx": {
6018+ "name": "survival_obs_cancer_idx",
6019+ "columns": [
6020+ {
6021+ "expression": "cancer_id",
6022+ "isExpression": false,
6023+ "asc": true,
6024+ "nulls": "last"
6025+ },
6026+ {
6027+ "expression": "survival_type",
6028+ "isExpression": false,
6029+ "asc": true,
6030+ "nulls": "last"
6031+ },
6032+ {
6033+ "expression": "duration_months",
6034+ "isExpression": false,
6035+ "asc": true,
6036+ "nulls": "last"
6037+ }
6038+ ],
6039+ "isUnique": false,
6040+ "concurrently": false,
6041+ "method": "btree",
6042+ "with": {}
6043+ }
6044+ },
6045+ "foreignKeys": {},
6046+ "compositePrimaryKeys": {},
6047+ "uniqueConstraints": {},
6048+ "policies": {},
6049+ "checkConstraints": {},
6050+ "isRLSEnabled": false
6051+ },
6052+ "public.ai_answers": {
6053+ "name": "ai_answers",
6054+ "schema": "",
6055+ "columns": {
6056+ "id": {
6057+ "name": "id",
6058+ "type": "bigserial",
6059+ "primaryKey": true,
6060+ "notNull": true
6061+ },
6062+ "kind": {
6063+ "name": "kind",
6064+ "type": "text",
6065+ "primaryKey": false,
6066+ "notNull": true
6067+ },
6068+ "subject_id": {
6069+ "name": "subject_id",
6070+ "type": "text",
6071+ "primaryKey": false,
6072+ "notNull": false
6073+ },
6074+ "question_hash": {
6075+ "name": "question_hash",
6076+ "type": "text",
6077+ "primaryKey": false,
6078+ "notNull": true
6079+ },
6080+ "question": {
6081+ "name": "question",
6082+ "type": "text",
6083+ "primaryKey": false,
6084+ "notNull": false
6085+ },
6086+ "answer": {
6087+ "name": "answer",
6088+ "type": "jsonb",
6089+ "primaryKey": false,
6090+ "notNull": true
6091+ },
6092+ "model": {
6093+ "name": "model",
6094+ "type": "text",
6095+ "primaryKey": false,
6096+ "notNull": true
6097+ },
6098+ "prompt_version": {
6099+ "name": "prompt_version",
6100+ "type": "text",
6101+ "primaryKey": false,
6102+ "notNull": true
6103+ },
6104+ "source_snapshot": {
6105+ "name": "source_snapshot",
6106+ "type": "jsonb",
6107+ "primaryKey": false,
6108+ "notNull": true,
6109+ "default": "'{}'::jsonb"
6110+ },
6111+ "data_as_of": {
6112+ "name": "data_as_of",
6113+ "type": "timestamp with time zone",
6114+ "primaryKey": false,
6115+ "notNull": true
6116+ },
6117+ "created_at": {
6118+ "name": "created_at",
6119+ "type": "timestamp with time zone",
6120+ "primaryKey": false,
6121+ "notNull": true,
6122+ "default": "now()"
6123+ }
6124+ },
6125+ "indexes": {
6126+ "ai_answers_uq": {
6127+ "name": "ai_answers_uq",
6128+ "columns": [
6129+ {
6130+ "expression": "kind",
6131+ "isExpression": false,
6132+ "asc": true,
6133+ "nulls": "last"
6134+ },
6135+ {
6136+ "expression": "question_hash",
6137+ "isExpression": false,
6138+ "asc": true,
6139+ "nulls": "last"
6140+ },
6141+ {
6142+ "expression": "prompt_version",
6143+ "isExpression": false,
6144+ "asc": true,
6145+ "nulls": "last"
6146+ }
6147+ ],
6148+ "isUnique": true,
6149+ "concurrently": false,
6150+ "method": "btree",
6151+ "with": {}
6152+ }
6153+ },
6154+ "foreignKeys": {},
6155+ "compositePrimaryKeys": {},
6156+ "uniqueConstraints": {},
6157+ "policies": {},
6158+ "checkConstraints": {},
6159+ "isRLSEnabled": false
6160+ },
6161+ "public.api_keys": {
6162+ "name": "api_keys",
6163+ "schema": "",
6164+ "columns": {
6165+ "id": {
6166+ "name": "id",
6167+ "type": "bigserial",
6168+ "primaryKey": true,
6169+ "notNull": true
6170+ },
6171+ "key_hash": {
6172+ "name": "key_hash",
6173+ "type": "text",
6174+ "primaryKey": false,
6175+ "notNull": true
6176+ },
6177+ "prefix": {
6178+ "name": "prefix",
6179+ "type": "text",
6180+ "primaryKey": false,
6181+ "notNull": true
6182+ },
6183+ "label": {
6184+ "name": "label",
6185+ "type": "text",
6186+ "primaryKey": false,
6187+ "notNull": false
6188+ },
6189+ "owner_email": {
6190+ "name": "owner_email",
6191+ "type": "text",
6192+ "primaryKey": false,
6193+ "notNull": false
6194+ },
6195+ "tier": {
6196+ "name": "tier",
6197+ "type": "text",
6198+ "primaryKey": false,
6199+ "notNull": true,
6200+ "default": "'free'"
6201+ },
6202+ "rate_limit_per_minute": {
6203+ "name": "rate_limit_per_minute",
6204+ "type": "integer",
6205+ "primaryKey": false,
6206+ "notNull": true,
6207+ "default": 60
6208+ },
6209+ "active": {
6210+ "name": "active",
6211+ "type": "boolean",
6212+ "primaryKey": false,
6213+ "notNull": true,
6214+ "default": true
6215+ },
6216+ "last_used_at": {
6217+ "name": "last_used_at",
6218+ "type": "timestamp with time zone",
6219+ "primaryKey": false,
6220+ "notNull": false
6221+ },
6222+ "created_at": {
6223+ "name": "created_at",
6224+ "type": "timestamp with time zone",
6225+ "primaryKey": false,
6226+ "notNull": true,
6227+ "default": "now()"
6228+ }
6229+ },
6230+ "indexes": {
6231+ "api_keys_hash_uq": {
6232+ "name": "api_keys_hash_uq",
6233+ "columns": [
6234+ {
6235+ "expression": "key_hash",
6236+ "isExpression": false,
6237+ "asc": true,
6238+ "nulls": "last"
6239+ }
6240+ ],
6241+ "isUnique": true,
6242+ "concurrently": false,
6243+ "method": "btree",
6244+ "with": {}
6245+ }
6246+ },
6247+ "foreignKeys": {},
6248+ "compositePrimaryKeys": {},
6249+ "uniqueConstraints": {},
6250+ "policies": {},
6251+ "checkConstraints": {},
6252+ "isRLSEnabled": false
6253+ },
6254+ "public.entity_counters": {
6255+ "name": "entity_counters",
6256+ "schema": "",
6257+ "columns": {
6258+ "id": {
6259+ "name": "id",
6260+ "type": "bigserial",
6261+ "primaryKey": true,
6262+ "notNull": true
6263+ },
6264+ "entity_type": {
6265+ "name": "entity_type",
6266+ "type": "text",
6267+ "primaryKey": false,
6268+ "notNull": true
6269+ },
6270+ "entity_id": {
6271+ "name": "entity_id",
6272+ "type": "text",
6273+ "primaryKey": false,
6274+ "notNull": true
6275+ },
6276+ "trial_count": {
6277+ "name": "trial_count",
6278+ "type": "integer",
6279+ "primaryKey": false,
6280+ "notNull": true,
6281+ "default": 0
6282+ },
6283+ "active_trial_count": {
6284+ "name": "active_trial_count",
6285+ "type": "integer",
6286+ "primaryKey": false,
6287+ "notNull": true,
6288+ "default": 0
6289+ },
6290+ "recruiting_trial_count": {
6291+ "name": "recruiting_trial_count",
6292+ "type": "integer",
6293+ "primaryKey": false,
6294+ "notNull": true,
6295+ "default": 0
6296+ },
6297+ "phase3_trial_count": {
6298+ "name": "phase3_trial_count",
6299+ "type": "integer",
6300+ "primaryKey": false,
6301+ "notNull": true,
6302+ "default": 0
6303+ },
6304+ "publication_count": {
6305+ "name": "publication_count",
6306+ "type": "integer",
6307+ "primaryKey": false,
6308+ "notNull": true,
6309+ "default": 0
6310+ },
6311+ "publication_count_5y": {
6312+ "name": "publication_count_5y",
6313+ "type": "integer",
6314+ "primaryKey": false,
6315+ "notNull": true,
6316+ "default": 0
6317+ },
6318+ "publication_count_12m": {
6319+ "name": "publication_count_12m",
6320+ "type": "integer",
6321+ "primaryKey": false,
6322+ "notNull": true,
6323+ "default": 0
6324+ },
6325+ "gene_count": {
6326+ "name": "gene_count",
6327+ "type": "integer",
6328+ "primaryKey": false,
6329+ "notNull": true,
6330+ "default": 0
6331+ },
6332+ "variant_count": {
6333+ "name": "variant_count",
6334+ "type": "integer",
6335+ "primaryKey": false,
6336+ "notNull": true,
6337+ "default": 0
6338+ },
6339+ "drug_count": {
6340+ "name": "drug_count",
6341+ "type": "integer",
6342+ "primaryKey": false,
6343+ "notNull": true,
6344+ "default": 0
6345+ },
6346+ "approved_drug_count": {
6347+ "name": "approved_drug_count",
6348+ "type": "integer",
6349+ "primaryKey": false,
6350+ "notNull": true,
6351+ "default": 0
6352+ },
6353+ "evidence_count": {
6354+ "name": "evidence_count",
6355+ "type": "integer",
6356+ "primaryKey": false,
6357+ "notNull": true,
6358+ "default": 0
6359+ },
6360+ "cohort_count": {
6361+ "name": "cohort_count",
6362+ "type": "integer",
6363+ "primaryKey": false,
6364+ "notNull": true,
6365+ "default": 0
6366+ },
6367+ "subtype_count": {
6368+ "name": "subtype_count",
6369+ "type": "integer",
6370+ "primaryKey": false,
6371+ "notNull": true,
6372+ "default": 0
6373+ },
6374+ "descendant_count": {
6375+ "name": "descendant_count",
6376+ "type": "integer",
6377+ "primaryKey": false,
6378+ "notNull": true,
6379+ "default": 0
6380+ },
6381+ "epidemiology_obs_count": {
6382+ "name": "epidemiology_obs_count",
6383+ "type": "integer",
6384+ "primaryKey": false,
6385+ "notNull": true,
6386+ "default": 0
6387+ },
6388+ "survival_obs_count": {
6389+ "name": "survival_obs_count",
6390+ "type": "integer",
6391+ "primaryKey": false,
6392+ "notNull": true,
6393+ "default": 0
6394+ },
6395+ "completeness": {
6396+ "name": "completeness",
6397+ "type": "jsonb",
6398+ "primaryKey": false,
6399+ "notNull": true,
6400+ "default": "'{}'::jsonb"
6401+ },
6402+ "updated_at": {
6403+ "name": "updated_at",
6404+ "type": "timestamp with time zone",
6405+ "primaryKey": false,
6406+ "notNull": true,
6407+ "default": "now()"
6408+ }
6409+ },
6410+ "indexes": {
6411+ "entity_counters_uq": {
6412+ "name": "entity_counters_uq",
6413+ "columns": [
6414+ {
6415+ "expression": "entity_type",
6416+ "isExpression": false,
6417+ "asc": true,
6418+ "nulls": "last"
6419+ },
6420+ {
6421+ "expression": "entity_id",
6422+ "isExpression": false,
6423+ "asc": true,
6424+ "nulls": "last"
6425+ }
6426+ ],
6427+ "isUnique": true,
6428+ "concurrently": false,
6429+ "method": "btree",
6430+ "with": {}
6431+ },
6432+ "entity_counters_trials_idx": {
6433+ "name": "entity_counters_trials_idx",
6434+ "columns": [
6435+ {
6436+ "expression": "entity_type",
6437+ "isExpression": false,
6438+ "asc": true,
6439+ "nulls": "last"
6440+ },
6441+ {
6442+ "expression": "active_trial_count",
6443+ "isExpression": false,
6444+ "asc": true,
6445+ "nulls": "last"
6446+ }
6447+ ],
6448+ "isUnique": false,
6449+ "concurrently": false,
6450+ "method": "btree",
6451+ "with": {}
6452+ }
6453+ },
6454+ "foreignKeys": {},
6455+ "compositePrimaryKeys": {},
6456+ "uniqueConstraints": {},
6457+ "policies": {},
6458+ "checkConstraints": {},
6459+ "isRLSEnabled": false
6460+ },
6461+ "public.metric_definitions": {
6462+ "name": "metric_definitions",
6463+ "schema": "",
6464+ "columns": {
6465+ "id": {
6466+ "name": "id",
6467+ "type": "varchar(32)",
6468+ "primaryKey": true,
6469+ "notNull": true
6470+ },
6471+ "slug": {
6472+ "name": "slug",
6473+ "type": "text",
6474+ "primaryKey": false,
6475+ "notNull": true
6476+ },
6477+ "name": {
6478+ "name": "name",
6479+ "type": "text",
6480+ "primaryKey": false,
6481+ "notNull": true
6482+ },
6483+ "description": {
6484+ "name": "description",
6485+ "type": "text",
6486+ "primaryKey": false,
6487+ "notNull": true
6488+ },
6489+ "formula": {
6490+ "name": "formula",
6491+ "type": "text",
6492+ "primaryKey": false,
6493+ "notNull": true
6494+ },
6495+ "formula_version": {
6496+ "name": "formula_version",
6497+ "type": "text",
6498+ "primaryKey": false,
6499+ "notNull": true
6500+ },
6501+ "unit": {
6502+ "name": "unit",
6503+ "type": "text",
6504+ "primaryKey": false,
6505+ "notNull": true
6506+ },
6507+ "higher_is_worse": {
6508+ "name": "higher_is_worse",
6509+ "type": "boolean",
6510+ "primaryKey": false,
6511+ "notNull": false
6512+ },
6513+ "aggregation": {
6514+ "name": "aggregation",
6515+ "type": "text",
6516+ "primaryKey": false,
6517+ "notNull": false
6518+ },
6519+ "valid_dimensions": {
6520+ "name": "valid_dimensions",
6521+ "type": "text[]",
6522+ "primaryKey": false,
6523+ "notNull": true,
6524+ "default": "'{}'"
6525+ },
6526+ "source_slugs": {
6527+ "name": "source_slugs",
6528+ "type": "text[]",
6529+ "primaryKey": false,
6530+ "notNull": true,
6531+ "default": "'{}'"
6532+ },
6533+ "category": {
6534+ "name": "category",
6535+ "type": "text",
6536+ "primaryKey": false,
6537+ "notNull": true
6538+ },
6539+ "eligibility": {
6540+ "name": "eligibility",
6541+ "type": "jsonb",
6542+ "primaryKey": false,
6543+ "notNull": true,
6544+ "default": "'{}'::jsonb"
6545+ },
6546+ "experimental": {
6547+ "name": "experimental",
6548+ "type": "boolean",
6549+ "primaryKey": false,
6550+ "notNull": true,
6551+ "default": false
6552+ },
6553+ "created_at": {
6554+ "name": "created_at",
6555+ "type": "timestamp with time zone",
6556+ "primaryKey": false,
6557+ "notNull": true,
6558+ "default": "now()"
6559+ },
6560+ "updated_at": {
6561+ "name": "updated_at",
6562+ "type": "timestamp with time zone",
6563+ "primaryKey": false,
6564+ "notNull": true,
6565+ "default": "now()"
6566+ }
6567+ },
6568+ "indexes": {
6569+ "metric_definitions_slug_uq": {
6570+ "name": "metric_definitions_slug_uq",
6571+ "columns": [
6572+ {
6573+ "expression": "slug",
6574+ "isExpression": false,
6575+ "asc": true,
6576+ "nulls": "last"
6577+ }
6578+ ],
6579+ "isUnique": true,
6580+ "concurrently": false,
6581+ "method": "btree",
6582+ "with": {}
6583+ }
6584+ },
6585+ "foreignKeys": {},
6586+ "compositePrimaryKeys": {},
6587+ "uniqueConstraints": {},
6588+ "policies": {},
6589+ "checkConstraints": {},
6590+ "isRLSEnabled": false
6591+ },
6592+ "public.ranking_snapshots": {
6593+ "name": "ranking_snapshots",
6594+ "schema": "",
6595+ "columns": {
6596+ "id": {
6597+ "name": "id",
6598+ "type": "bigserial",
6599+ "primaryKey": true,
6600+ "notNull": true
6601+ },
6602+ "metric_id": {
6603+ "name": "metric_id",
6604+ "type": "varchar(32)",
6605+ "primaryKey": false,
6606+ "notNull": true
6607+ },
6608+ "metric_slug": {
6609+ "name": "metric_slug",
6610+ "type": "text",
6611+ "primaryKey": false,
6612+ "notNull": true
6613+ },
6614+ "scope_key": {
6615+ "name": "scope_key",
6616+ "type": "text",
6617+ "primaryKey": false,
6618+ "notNull": true
6619+ },
6620+ "geography": {
6621+ "name": "geography",
6622+ "type": "text",
6623+ "primaryKey": false,
6624+ "notNull": true,
6625+ "default": "'WORLD'"
6626+ },
6627+ "sex": {
6628+ "name": "sex",
6629+ "type": "text",
6630+ "primaryKey": false,
6631+ "notNull": true,
6632+ "default": "'all'"
6633+ },
6634+ "age_group": {
6635+ "name": "age_group",
6636+ "type": "text",
6637+ "primaryKey": false,
6638+ "notNull": true,
6639+ "default": "'all'"
6640+ },
6641+ "year": {
6642+ "name": "year",
6643+ "type": "integer",
6644+ "primaryKey": false,
6645+ "notNull": false
6646+ },
6647+ "entity_level": {
6648+ "name": "entity_level",
6649+ "type": "text",
6650+ "primaryKey": false,
6651+ "notNull": true,
6652+ "default": "'top'"
6653+ },
6654+ "formula_version": {
6655+ "name": "formula_version",
6656+ "type": "text",
6657+ "primaryKey": false,
6658+ "notNull": true
6659+ },
6660+ "eligible_entities": {
6661+ "name": "eligible_entities",
6662+ "type": "integer",
6663+ "primaryKey": false,
6664+ "notNull": true
6665+ },
6666+ "inputs_hash": {
6667+ "name": "inputs_hash",
6668+ "type": "text",
6669+ "primaryKey": false,
6670+ "notNull": true
6671+ },
6672+ "source_ids": {
6673+ "name": "source_ids",
6674+ "type": "text[]",
6675+ "primaryKey": false,
6676+ "notNull": true,
6677+ "default": "'{}'"
6678+ },
6679+ "is_current": {
6680+ "name": "is_current",
6681+ "type": "boolean",
6682+ "primaryKey": false,
6683+ "notNull": true,
6684+ "default": true
6685+ },
6686+ "generated_at": {
6687+ "name": "generated_at",
6688+ "type": "timestamp with time zone",
6689+ "primaryKey": false,
6690+ "notNull": true,
6691+ "default": "now()"
6692+ }
6693+ },
6694+ "indexes": {
6695+ "ranking_snapshots_lookup_idx": {
6696+ "name": "ranking_snapshots_lookup_idx",
6697+ "columns": [
6698+ {
6699+ "expression": "metric_slug",
6700+ "isExpression": false,
6701+ "asc": true,
6702+ "nulls": "last"
6703+ },
6704+ {
6705+ "expression": "scope_key",
6706+ "isExpression": false,
6707+ "asc": true,
6708+ "nulls": "last"
6709+ },
6710+ {
6711+ "expression": "is_current",
6712+ "isExpression": false,
6713+ "asc": true,
6714+ "nulls": "last"
6715+ }
6716+ ],
6717+ "isUnique": false,
6718+ "concurrently": false,
6719+ "method": "btree",
6720+ "with": {}
6721+ }
6722+ },
6723+ "foreignKeys": {},
6724+ "compositePrimaryKeys": {},
6725+ "uniqueConstraints": {},
6726+ "policies": {},
6727+ "checkConstraints": {},
6728+ "isRLSEnabled": false
6729+ },
6730+ "public.rankings": {
6731+ "name": "rankings",
6732+ "schema": "",
6733+ "columns": {
6734+ "id": {
6735+ "name": "id",
6736+ "type": "bigserial",
6737+ "primaryKey": true,
6738+ "notNull": true
6739+ },
6740+ "snapshot_id": {
6741+ "name": "snapshot_id",
6742+ "type": "integer",
6743+ "primaryKey": false,
6744+ "notNull": true
6745+ },
6746+ "metric_slug": {
6747+ "name": "metric_slug",
6748+ "type": "text",
6749+ "primaryKey": false,
6750+ "notNull": true
6751+ },
6752+ "scope_key": {
6753+ "name": "scope_key",
6754+ "type": "text",
6755+ "primaryKey": false,
6756+ "notNull": true
6757+ },
6758+ "cancer_id": {
6759+ "name": "cancer_id",
6760+ "type": "varchar(32)",
6761+ "primaryKey": false,
6762+ "notNull": true
6763+ },
6764+ "rank": {
6765+ "name": "rank",
6766+ "type": "integer",
6767+ "primaryKey": false,
6768+ "notNull": true
6769+ },
6770+ "eligible_entities": {
6771+ "name": "eligible_entities",
6772+ "type": "integer",
6773+ "primaryKey": false,
6774+ "notNull": true
6775+ },
6776+ "percentile": {
6777+ "name": "percentile",
6778+ "type": "real",
6779+ "primaryKey": false,
6780+ "notNull": true
6781+ },
6782+ "value": {
6783+ "name": "value",
6784+ "type": "double precision",
6785+ "primaryKey": false,
6786+ "notNull": true
6787+ },
6788+ "unit": {
6789+ "name": "unit",
6790+ "type": "text",
6791+ "primaryKey": false,
6792+ "notNull": true
6793+ },
6794+ "confidence": {
6795+ "name": "confidence",
6796+ "type": "text",
6797+ "primaryKey": false,
6798+ "notNull": true,
6799+ "default": "'MEDIUM'"
6800+ },
6801+ "inputs": {
6802+ "name": "inputs",
6803+ "type": "jsonb",
6804+ "primaryKey": false,
6805+ "notNull": true,
6806+ "default": "'{}'::jsonb"
6807+ },
6808+ "breakdown": {
6809+ "name": "breakdown",
6810+ "type": "jsonb",
6811+ "primaryKey": false,
6812+ "notNull": false
6813+ },
6814+ "previous_rank": {
6815+ "name": "previous_rank",
6816+ "type": "integer",
6817+ "primaryKey": false,
6818+ "notNull": false
6819+ },
6820+ "generated_at": {
6821+ "name": "generated_at",
6822+ "type": "timestamp with time zone",
6823+ "primaryKey": false,
6824+ "notNull": true,
6825+ "default": "now()"
6826+ }
6827+ },
6828+ "indexes": {
6829+ "rankings_uq": {
6830+ "name": "rankings_uq",
6831+ "columns": [
6832+ {
6833+ "expression": "snapshot_id",
6834+ "isExpression": false,
6835+ "asc": true,
6836+ "nulls": "last"
6837+ },
6838+ {
6839+ "expression": "cancer_id",
6840+ "isExpression": false,
6841+ "asc": true,
6842+ "nulls": "last"
6843+ }
6844+ ],
6845+ "isUnique": true,
6846+ "concurrently": false,
6847+ "method": "btree",
6848+ "with": {}
6849+ },
6850+ "rankings_cancer_idx": {
6851+ "name": "rankings_cancer_idx",
6852+ "columns": [
6853+ {
6854+ "expression": "cancer_id",
6855+ "isExpression": false,
6856+ "asc": true,
6857+ "nulls": "last"
6858+ },
6859+ {
6860+ "expression": "metric_slug",
6861+ "isExpression": false,
6862+ "asc": true,
6863+ "nulls": "last"
6864+ }
6865+ ],
6866+ "isUnique": false,
6867+ "concurrently": false,
6868+ "method": "btree",
6869+ "with": {}
6870+ },
6871+ "rankings_lookup_idx": {
6872+ "name": "rankings_lookup_idx",
6873+ "columns": [
6874+ {
6875+ "expression": "metric_slug",
6876+ "isExpression": false,
6877+ "asc": true,
6878+ "nulls": "last"
6879+ },
6880+ {
6881+ "expression": "scope_key",
6882+ "isExpression": false,
6883+ "asc": true,
6884+ "nulls": "last"
6885+ },
6886+ {
6887+ "expression": "rank",
6888+ "isExpression": false,
6889+ "asc": true,
6890+ "nulls": "last"
6891+ }
6892+ ],
6893+ "isUnique": false,
6894+ "concurrently": false,
6895+ "method": "btree",
6896+ "with": {}
6897+ }
6898+ },
6899+ "foreignKeys": {},
6900+ "compositePrimaryKeys": {},
6901+ "uniqueConstraints": {},
6902+ "policies": {},
6903+ "checkConstraints": {},
6904+ "isRLSEnabled": false
6905+ }
6906+ },
6907+ "enums": {},
6908+ "schemas": {},
6909+ "sequences": {},
6910+ "roles": {},
6911+ "policies": {},
6912+ "views": {},
6913+ "_meta": {
6914+ "columns": {},
6915+ "schemas": {},
6916+ "tables": {}
6917+ }
6918+}
\ No newline at end of file
added packages/database/migrations/meta/_journal.json +13 −0
@@ -0,0 +1,13 @@
1+{
2+ "version": "7",
3+ "dialect": "postgresql",
4+ "entries": [
5+ {
6+ "idx": 0,
7+ "version": "7",
8+ "when": 1788858213248,
9+ "tag": "0000_small_blazing_skull",
10+ "breakpoints": true
11+ }
12+ ]
13+}
\ No newline at end of file
added packages/database/package.json +37 −0
@@ -0,0 +1,37 @@
1+{
2+ "name": "@cancerindex/database",
3+ "version": "0.1.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": {
8+ "types": "./src/index.ts",
9+ "default": "./src/index.ts"
10+ },
11+ "./schema": {
12+ "types": "./src/schema/index.ts",
13+ "default": "./src/schema/index.ts"
14+ }
15+ },
16+ "scripts": {
17+ "build": "tsc -p tsconfig.json --noEmit",
18+ "typecheck": "tsc -p tsconfig.json --noEmit",
19+ "test": "vitest run --passWithNoTests",
20+ "generate": "drizzle-kit generate",
21+ "migrate": "tsx src/migrate.ts",
22+ "seed": "tsx src/seed.ts",
23+ "studio": "drizzle-kit studio"
24+ },
25+ "dependencies": {
26+ "@cancerindex/shared": "workspace:*",
27+ "drizzle-orm": "^0.45.0",
28+ "postgres": "^3.4.7"
29+ },
30+ "devDependencies": {
31+ "@types/node": "^24.0.0",
32+ "drizzle-kit": "^0.31.0",
33+ "tsx": "^4.20.0",
34+ "typescript": "^5.9.3",
35+ "vitest": "^3.2.0"
36+ }
37+}
added packages/database/src/client.ts +42 −0
@@ -0,0 +1,42 @@
1+import postgres from 'postgres';
2+import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
3+import * as schema from './schema/index.js';
4+
5+export type Database = PostgresJsDatabase<typeof schema>;
6+
7+let _sql: ReturnType<typeof postgres> | null = null;
8+let _db: Database | null = null;
9+
10+export interface DbOptions {
11+ url?: string;
12+ max?: number;
13+}
14+
15+/** Lazily created singleton; safe from Next.js server components, Fastify and workers alike. */
16+export function getSql(opts: DbOptions = {}) {
17+ if (!_sql) {
18+ const url = opts.url ?? process.env.DATABASE_URL ?? 'postgres://localhost:5432/cancerindex';
19+ _sql = postgres(url, {
20+ max: opts.max ?? Number(process.env.DB_POOL_MAX ?? 10),
21+ idle_timeout: 30,
22+ connect_timeout: 10,
23+ prepare: true,
24+ transform: { undefined: null },
25+ onnotice: () => {},
26+ });
27+ }
28+ return _sql;
29+}
30+
31+export function getDb(opts: DbOptions = {}): Database {
32+ if (!_db) _db = drizzle(getSql(opts), { schema, casing: 'snake_case' });
33+ return _db;
34+}
35+
36+export async function closeDb(): Promise<void> {
37+ if (_sql) {
38+ await _sql.end({ timeout: 5 });
39+ _sql = null;
40+ _db = null;
41+ }
42+}
added packages/database/src/ids.ts +25 −0
@@ -0,0 +1,25 @@
1+import { sql } from 'drizzle-orm';
2+import { formatId, type IdNamespace } from '@cancerindex/shared';
3+import type { Database } from './client.js';
4+
5+/**
6+ * Mint stable public identifiers from per-namespace counters (CLAUDE.md §6).
7+ * Atomic upsert-increment; `count` ids are reserved in one round trip.
8+ */
9+export async function mintIds(db: Database, ns: IdNamespace, count = 1): Promise<string[]> {
10+ if (count < 1) return [];
11+ const rows = await db.execute<{ next: string | number }>(sql`
12+ INSERT INTO id_sequences (namespace, next) VALUES (${ns}, ${count + 1})
13+ ON CONFLICT (namespace) DO UPDATE SET next = id_sequences.next + ${count}
14+ RETURNING next
15+ `);
16+ const next = Number(rows[0]?.next ?? 0);
17+ const first = next - count;
18+ return Array.from({ length: count }, (_, i) => formatId(ns, first + i));
19+}
20+
21+export async function mintId(db: Database, ns: IdNamespace): Promise<string> {
22+ const [id] = await mintIds(db, ns, 1);
23+ if (!id) throw new Error('mint failed');
24+ return id;
25+}
added packages/database/src/index.ts +5 −0
@@ -0,0 +1,5 @@
1+export * from './client.js';
2+export * from './ids.js';
3+export * as schema from './schema/index.js';
4+export * from './schema/index.js';
5+export { runMigrations } from './migrate.js';
added packages/database/src/migrate.ts +40 −0
@@ -0,0 +1,40 @@
1+import { migrate } from 'drizzle-orm/postgres-js/migrator';
2+import { fileURLToPath } from 'node:url';
3+import path from 'node:path';
4+import { getDb, getSql, closeDb } from './client.js';
5+
6+/**
7+ * Runs SQL migrations from ./migrations (CLAUDE.md §284). Extensions are created first because
8+ * drizzle-kit does not manage them; pgvector is optional and only enabled when available.
9+ */
10+export async function runMigrations(): Promise<void> {
11+ const sql = getSql({ max: 1 });
12+ await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`;
13+ await sql`CREATE EXTENSION IF NOT EXISTS unaccent`;
14+ const [vec] = await sql`SELECT 1 AS ok FROM pg_available_extensions WHERE name = 'vector'`;
15+ if (vec) await sql`CREATE EXTENSION IF NOT EXISTS vector`;
16+ else console.warn('[migrate] pgvector not available — semantic search disabled until installed');
17+ const here = path.dirname(fileURLToPath(import.meta.url));
18+ await migrate(getDb({ max: 1 }), { migrationsFolder: path.resolve(here, '../migrations') });
19+ // Trigram indexes for fuzzy search (not expressible in drizzle schema).
20+ await sql`CREATE INDEX IF NOT EXISTS cancer_aliases_trgm_idx ON cancer_aliases USING gin (normalized gin_trgm_ops)`;
21+ await sql`CREATE INDEX IF NOT EXISTS cancers_name_trgm_idx ON cancers USING gin (lower(canonical_name) gin_trgm_ops)`;
22+ await sql`CREATE INDEX IF NOT EXISTS genes_symbol_trgm_idx ON genes USING gin (lower(symbol) gin_trgm_ops)`;
23+ await sql`CREATE INDEX IF NOT EXISTS drugs_name_trgm_idx ON drugs USING gin (lower(name) gin_trgm_ops)`;
24+ await sql`CREATE INDEX IF NOT EXISTS trials_title_trgm_idx ON clinical_trials USING gin (lower(brief_title) gin_trgm_ops)`;
25+ await sql`CREATE INDEX IF NOT EXISTS publications_title_trgm_idx ON publications USING gin (lower(title) gin_trgm_ops)`;
26+}
27+
28+const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
29+if (isMain) {
30+ runMigrations()
31+ .then(async () => {
32+ console.log('[migrate] done');
33+ await closeDb();
34+ })
35+ .catch(async (err) => {
36+ console.error('[migrate] failed', err);
37+ await closeDb();
38+ process.exit(1);
39+ });
40+}
added packages/database/src/schema/_common.ts +23 −0
@@ -0,0 +1,23 @@
1+import { timestamp, varchar, customType } from 'drizzle-orm/pg-core';
2+
3+export const createdAt = () => timestamp('created_at', { withTimezone: true }).notNull().defaultNow();
4+export const updatedAt = () => timestamp('updated_at', { withTimezone: true }).notNull().defaultNow();
5+
6+/** Public CancerIndex identifier column (CI-CAN-00000001 …). */
7+export const ciId = (name = 'id') => varchar(name, { length: 32 });
8+
9+/** pgvector column (embedding model is stored alongside, never mix models in one column without it). */
10+export const vector = customType<{ data: number[]; driverData: string; config: { dimensions: number } }>({
11+ dataType(config) {
12+ return `vector(${config?.dimensions ?? 1536})`;
13+ },
14+ toDriver(value) {
15+ return `[${value.join(',')}]`;
16+ },
17+ fromDriver(value) {
18+ return value
19+ .slice(1, -1)
20+ .split(',')
21+ .map((v) => Number(v));
22+ },
23+});
added packages/database/src/schema/cancers.ts +139 −0
@@ -0,0 +1,139 @@
1+import { pgTable, text, integer, boolean, bigserial, index, uniqueIndex, real, jsonb } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Canonical cancer entity (CLAUDE.md §5). One row per recognized disease concept, never per alias. */
5+export const cancers = pgTable(
6+ 'cancers',
7+ {
8+ id: ciId().primaryKey(), // CI-CAN-…
9+ slug: text('slug').notNull(),
10+ canonicalName: text('canonical_name').notNull(),
11+ shortName: text('short_name'),
12+ entityType: text('entity_type').notNull().default('cancer'), // cancer | cancer_family | histology | subtype | molecular_subtype | hematologic_malignancy | precursor_condition | other
13+ malignant: boolean('malignant').notNull().default(true),
14+ solidTumor: boolean('solid_tumor').notNull().default(true),
15+ hematologic: boolean('hematologic').notNull().default(false),
16+ pediatricRelevant: boolean('pediatric_relevant').notNull().default(false),
17+ rareCancer: boolean('rare_cancer'), // null = unknown (no incidence data yet)
18+ topLevel: boolean('top_level').notNull().default(false), // member of the mutually exclusive global ranking set (§247)
19+ description: text('description'),
20+ descriptionProvenanceId: integer('description_provenance_id'),
21+ primaryNcitCode: text('primary_ncit_code'),
22+ primaryOncotreeCode: text('primary_oncotree_code'),
23+ depth: integer('depth').notNull().default(0), // depth in the NCIt-derived hierarchy from root "Neoplasm"
24+ status: text('status').notNull().default('active'), // active | deprecated | merged
25+ mergedInto: ciId('merged_into'),
26+ deprecatedReason: text('deprecated_reason'),
27+ classificationVersion: text('classification_version'),
28+ semanticTypes: text('semantic_types').array().notNull().default([]),
29+ createdAt: createdAt(),
30+ updatedAt: updatedAt(),
31+ },
32+ (t) => [
33+ uniqueIndex('cancers_slug_uq').on(t.slug),
34+ uniqueIndex('cancers_ncit_uq').on(t.primaryNcitCode),
35+ index('cancers_name_idx').on(t.canonicalName),
36+ index('cancers_type_idx').on(t.entityType, t.malignant, t.topLevel),
37+ ],
38+);
39+
40+export const cancerAliases = pgTable(
41+ 'cancer_aliases',
42+ {
43+ id: bigserial('id', { mode: 'number' }).primaryKey(),
44+ cancerId: ciId('cancer_id').notNull(),
45+ alias: text('alias').notNull(),
46+ normalized: text('normalized').notNull(),
47+ aliasType: text('alias_type').notNull().default('synonym'), // preferred | synonym | abbreviation | historical | deprecated | display
48+ sourceId: ciId('source_id'),
49+ sourceTerminology: text('source_terminology'), // NCIt synonym source (e.g. CTRP, caDSR) or connector id
50+ language: text('language').notNull().default('en'),
51+ },
52+ (t) => [uniqueIndex('cancer_aliases_uq').on(t.cancerId, t.normalized, t.aliasType), index('cancer_aliases_norm_idx').on(t.normalized)],
53+);
54+
55+/** Multi-dimensional hierarchy (CLAUDE.md §4): several trees coexist. */
56+export const cancerHierarchy = pgTable(
57+ 'cancer_hierarchy',
58+ {
59+ id: bigserial('id', { mode: 'number' }).primaryKey(),
60+ parentId: ciId('parent_id').notNull(),
61+ childId: ciId('child_id').notNull(),
62+ hierarchyType: text('hierarchy_type').notNull(), // ncit | oncotree | anatomical | histological | molecular | who | icd | seer
63+ sourceId: ciId('source_id'),
64+ },
65+ (t) => [uniqueIndex('cancer_hierarchy_uq').on(t.parentId, t.childId, t.hierarchyType), index('cancer_hierarchy_child_idx').on(t.childId)],
66+);
67+
68+/** Cross-reference codes (CLAUDE.md §220-221, §347): searchable, never buried in JSON. */
69+export const cancerCodes = pgTable(
70+ 'cancer_codes',
71+ {
72+ id: bigserial('id', { mode: 'number' }).primaryKey(),
73+ cancerId: ciId('cancer_id').notNull(),
74+ system: text('system').notNull(), // ncit | icd10 | icd10cm | icdo_topography | icdo_morphology | doid | oncotree | umls | mesh | mondo | seer_site | efo | orphanet | gdc_project
75+ code: text('code').notNull(),
76+ matchType: text('match_type').notNull().default('EXACT_IDENTIFIER'),
77+ sourceId: ciId('source_id'),
78+ validFrom: text('valid_from'),
79+ validTo: text('valid_to'),
80+ },
81+ (t) => [uniqueIndex('cancer_codes_uq').on(t.cancerId, t.system, t.code), index('cancer_codes_lookup_idx').on(t.system, t.code)],
82+);
83+
84+export const anatomicalSites = pgTable(
85+ 'anatomical_sites',
86+ {
87+ id: ciId().primaryKey(), // CI-ANAT-…
88+ name: text('name').notNull(),
89+ slug: text('slug').notNull(),
90+ ncitCode: text('ncit_code'),
91+ uberonId: text('uberon_id'),
92+ parentId: ciId('parent_id'),
93+ system: text('system'), // e.g. Digestive, Respiratory, Hematopoietic
94+ },
95+ (t) => [uniqueIndex('anatomical_sites_slug_uq').on(t.slug)],
96+);
97+
98+export const cancerAnatomy = pgTable(
99+ 'cancer_anatomy',
100+ {
101+ id: bigserial('id', { mode: 'number' }).primaryKey(),
102+ cancerId: ciId('cancer_id').notNull(),
103+ siteId: ciId('site_id').notNull(),
104+ relation: text('relation').notNull().default('primary'), // primary | metastatic
105+ sourceId: ciId('source_id'),
106+ },
107+ (t) => [uniqueIndex('cancer_anatomy_uq').on(t.cancerId, t.siteId, t.relation)],
108+);
109+
110+/** Canonical geography (CLAUDE.md §117). */
111+export const geographies = pgTable(
112+ 'geographies',
113+ {
114+ id: ciId().primaryKey(), // CI-GEO-…
115+ slug: text('slug').notNull(),
116+ name: text('name').notNull(),
117+ kind: text('kind').notNull(), // world | region | who_region | country | subdivision
118+ iso2: text('iso2'),
119+ iso3: text('iso3'),
120+ parentId: ciId('parent_id'),
121+ whoRegion: text('who_region'),
122+ population: integer('population'),
123+ populationYear: integer('population_year'),
124+ },
125+ (t) => [uniqueIndex('geographies_slug_uq').on(t.slug), index('geographies_iso3_idx').on(t.iso3)],
126+);
127+
128+/** Cohort definitions (CLAUDE.md §218): attribute combinations that are not taxonomy nodes. */
129+export const cohortDefinitions = pgTable('cohort_definitions', {
130+ id: bigserial('id', { mode: 'number' }).primaryKey(),
131+ name: text('name').notNull(),
132+ cancerId: ciId('cancer_id').notNull(),
133+ biomarkerIds: text('biomarker_ids').array().notNull().default([]),
134+ variantIds: text('variant_ids').array().notNull().default([]),
135+ stage: text('stage'),
136+ attributes: jsonb('attributes').$type<Record<string, unknown>>().notNull().default({}),
137+ confidence: real('confidence'),
138+ createdAt: createdAt(),
139+});
added packages/database/src/schema/drugs.ts +84 −0
@@ -0,0 +1,84 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, boolean, jsonb } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Drugs (CLAUDE.md §7, §344): brand names are aliases, never separate molecules. */
5+export const drugs = pgTable(
6+ 'drugs',
7+ {
8+ id: ciId().primaryKey(), // CI-DRUG-…
9+ slug: text('slug').notNull(),
10+ name: text('name').notNull(), // generic / INN preferred
11+ kind: text('kind'), // small_molecule | monoclonal_antibody | adc | bispecific | car_t | cell_therapy | gene_therapy | vaccine | radiopharmaceutical | chemotherapy | hormonal | immunotherapy | targeted | other
12+ ncitCode: text('ncit_code'),
13+ chemblId: text('chembl_id'),
14+ civicTherapyId: integer('civic_therapy_id'),
15+ drugbankId: text('drugbank_id'),
16+ pubchemCid: text('pubchem_cid'),
17+ unii: text('unii'),
18+ mechanism: text('mechanism'),
19+ targetGeneIds: text('target_gene_ids').array().notNull().default([]),
20+ developmentStatus: text('development_status'),
21+ description: text('description'),
22+ createdAt: createdAt(),
23+ updatedAt: updatedAt(),
24+ },
25+ (t) => [uniqueIndex('drugs_slug_uq').on(t.slug), index('drugs_ncit_idx').on(t.ncitCode), index('drugs_civic_idx').on(t.civicTherapyId), index('drugs_chembl_idx').on(t.chemblId)],
26+);
27+
28+export const drugAliases = pgTable(
29+ 'drug_aliases',
30+ {
31+ id: bigserial('id', { mode: 'number' }).primaryKey(),
32+ drugId: ciId('drug_id').notNull(),
33+ alias: text('alias').notNull(),
34+ normalized: text('normalized').notNull(),
35+ aliasType: text('alias_type').notNull().default('synonym'), // generic | brand | development_code | salt | synonym
36+ sourceId: ciId('source_id'),
37+ },
38+ (t) => [uniqueIndex('drug_aliases_uq').on(t.drugId, t.normalized, t.aliasType), index('drug_aliases_norm_idx').on(t.normalized)],
39+);
40+
41+/** Regimens / combinations (CLAUDE.md §53). */
42+export const treatmentRegimens = pgTable(
43+ 'treatment_regimens',
44+ {
45+ id: ciId().primaryKey(), // CI-TRT-…
46+ slug: text('slug').notNull(),
47+ name: text('name').notNull(),
48+ componentDrugIds: text('component_drug_ids').array().notNull().default([]),
49+ modality: text('modality').notNull().default('drug_combination'), // drug | drug_combination | surgery | radiotherapy | transplantation | cell_therapy | surveillance
50+ description: text('description'),
51+ createdAt: createdAt(),
52+ },
53+ (t) => [uniqueIndex('treatment_regimens_slug_uq').on(t.slug)],
54+);
55+
56+/** Country-aware regulatory status (CLAUDE.md §13). Never a bare approved=true. */
57+export const drugApprovals = pgTable(
58+ 'drug_approvals',
59+ {
60+ id: bigserial('id', { mode: 'number' }).primaryKey(),
61+ drugId: ciId('drug_id').notNull(),
62+ cancerId: ciId('cancer_id'),
63+ biomarkerIds: text('biomarker_ids').array().notNull().default([]),
64+ tumorAgnostic: boolean('tumor_agnostic').notNull().default(false),
65+ jurisdiction: text('jurisdiction').notNull(), // US | CA | EU | UK | AU | JP | CH | OTHER
66+ authority: text('authority').notNull(), // FDA | Health Canada | EMA | MHRA | TGA | PMDA | Swissmedic
67+ indication: text('indication').notNull(),
68+ lineOfTherapy: text('line_of_therapy'),
69+ diseaseStage: text('disease_stage'),
70+ approvalType: text('approval_type'),
71+ accelerated: boolean('accelerated'),
72+ conditional: boolean('conditional'),
73+ approvalDate: text('approval_date'),
74+ withdrawalDate: text('withdrawal_date'),
75+ status: text('status').notNull(), // approved | conditional | accelerated | withdrawn | superseded
76+ applicationNumber: text('application_number'),
77+ sourceId: ciId('source_id').notNull(),
78+ provenanceId: integer('provenance_id').notNull(),
79+ raw: jsonb('raw'),
80+ createdAt: createdAt(),
81+ updatedAt: updatedAt(),
82+ },
83+ (t) => [index('drug_approvals_drug_idx').on(t.drugId), index('drug_approvals_cancer_idx').on(t.cancerId)],
84+);
added packages/database/src/schema/epidemiology.ts +61 −0
@@ -0,0 +1,61 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, real, doublePrecision } from 'drizzle-orm/pg-core';
2+import { ciId, updatedAt } from './_common.js';
3+
4+/** Time-aware epidemiology observations (CLAUDE.md §71): never overwrite a year with another. */
5+export const epidemiologyObservations = pgTable(
6+ 'epidemiology_observations',
7+ {
8+ id: bigserial('id', { mode: 'number' }).primaryKey(),
9+ cancerId: ciId('cancer_id').notNull(),
10+ geographyId: ciId('geography_id').notNull(),
11+ year: integer('year').notNull(),
12+ yearEnd: integer('year_end'), // for multi-year aggregates (e.g. 2018-2022)
13+ sex: text('sex').notNull().default('all'), // all | male | female
14+ ageGroup: text('age_group').notNull().default('all'),
15+ metric: text('metric').notNull(), // incidence_count | incidence_rate | as_incidence_rate | mortality_count | mortality_rate | as_mortality_rate | prevalence | prevalence_5y
16+ value: doublePrecision('value').notNull(),
17+ unit: text('unit').notNull(), // count | per_100k
18+ lowerCi: doublePrecision('lower_ci'),
19+ upperCi: doublePrecision('upper_ci'),
20+ standardPopulation: text('standard_population'), // e.g. "US 2000 standard", "World (Segi)"
21+ estimateType: text('estimate_type').notNull().default('observed'), // observed | estimated | projected
22+ siteDefinition: text('site_definition'), // source's own site label / ICD code range
23+ sourceId: ciId('source_id').notNull(),
24+ provenanceId: integer('provenance_id').notNull(),
25+ ingestRunId: text('ingest_run_id'),
26+ updatedAt: updatedAt(),
27+ },
28+ (t) => [
29+ uniqueIndex('epi_obs_uq').on(t.cancerId, t.geographyId, t.year, t.sex, t.ageGroup, t.metric, t.sourceId, t.siteDefinition),
30+ index('epi_obs_lookup_idx').on(t.metric, t.geographyId, t.year, t.sex),
31+ index('epi_obs_cancer_idx').on(t.cancerId, t.metric),
32+ ],
33+);
34+
35+/** Survival requires context (CLAUDE.md §72). */
36+export const survivalObservations = pgTable(
37+ 'survival_observations',
38+ {
39+ id: bigserial('id', { mode: 'number' }).primaryKey(),
40+ cancerId: ciId('cancer_id').notNull(),
41+ geographyId: ciId('geography_id'),
42+ stage: text('stage'),
43+ stagingSystem: text('staging_system'),
44+ sex: text('sex').notNull().default('all'),
45+ ageGroup: text('age_group').notNull().default('all'),
46+ diagnosisPeriod: text('diagnosis_period'),
47+ survivalType: text('survival_type').notNull(), // overall | relative | cause_specific | progression_free | disease_free | net
48+ durationMonths: integer('duration_months').notNull(),
49+ probability: real('probability'), // 0..1
50+ medianMonths: real('median_months'),
51+ cohortSize: integer('cohort_size'),
52+ lowerCi: real('lower_ci'),
53+ upperCi: real('upper_ci'),
54+ method: text('method'),
55+ sourceId: ciId('source_id').notNull(),
56+ provenanceId: integer('provenance_id').notNull(),
57+ ingestRunId: text('ingest_run_id'),
58+ updatedAt: updatedAt(),
59+ },
60+ (t) => [index('survival_obs_cancer_idx').on(t.cancerId, t.survivalType, t.durationMonths)],
61+);
added packages/database/src/schema/evidence.ts +89 −0
@@ -0,0 +1,89 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, boolean, jsonb, real, timestamp } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Knowledge graph edge with mandatory context and provenance (CLAUDE.md §28-29, §244). */
5+export const knowledgeEdges = pgTable(
6+ 'knowledge_edges',
7+ {
8+ id: bigserial('id', { mode: 'number' }).primaryKey(),
9+ sourceEntityType: text('source_entity_type').notNull(), // cancer | gene | variant | drug | biomarker | trial | publication | pathway | risk_factor
10+ sourceEntityId: text('source_entity_id').notNull(),
11+ targetEntityType: text('target_entity_type').notNull(),
12+ targetEntityId: text('target_entity_id').notNull(),
13+ relationshipType: text('relationship_type').notNull(), // HAS_SUBTYPE | OCCURS_IN | ASSOCIATED_WITH | HAS_VARIANT | HAS_BIOMARKER | TREATED_BY | STUDIED_IN | DESCRIBED_BY | TARGETS | APPROVED_FOR | INVESTIGATED_FOR | PREDICTS_RESPONSE_TO | CONFERS_RESISTANCE_TO | PREDISPOSES_TO | PROGNOSTIC_IN | DIAGNOSTIC_OF
14+ cancerContextIds: text('cancer_context_ids').array().notNull().default([]),
15+ predictive: boolean('predictive'),
16+ prognostic: boolean('prognostic'),
17+ diagnostic: boolean('diagnostic'),
18+ predisposing: boolean('predisposing'),
19+ direction: text('direction'), // supports | resistance | sensitivity | neutral | unknown
20+ evidenceLevel: text('evidence_level'), // source-native level (e.g. CIViC A-E) — never re-scaled silently
21+ evidenceScore: real('evidence_score'),
22+ evidenceCategory: text('evidence_category').notNull().default('curated_evidence'), // observed_data | published_evidence | curated_evidence | regulatory_status | clinical_guideline | computed_metric
23+ status: text('status').notNull().default('active'), // active | candidate | superseded | retracted_basis
24+ sourceId: ciId('source_id').notNull(),
25+ sourceRecordId: text('source_record_id'),
26+ provenanceIds: integer('provenance_ids').array().notNull().default([]),
27+ supportCount: integer('support_count').notNull().default(1),
28+ firstSeenAt: timestamp('first_seen_at', { withTimezone: true }).notNull().defaultNow(),
29+ lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow(),
30+ },
31+ (t) => [
32+ uniqueIndex('knowledge_edges_uq').on(t.sourceEntityType, t.sourceEntityId, t.targetEntityType, t.targetEntityId, t.relationshipType, t.sourceId, t.sourceRecordId),
33+ index('knowledge_edges_source_idx').on(t.sourceEntityType, t.sourceEntityId, t.relationshipType),
34+ index('knowledge_edges_target_idx').on(t.targetEntityType, t.targetEntityId, t.relationshipType),
35+ ],
36+);
37+
38+/** CIViC evidence items kept in their native structure (CLAUDE.md §10.9). */
39+export const civicEvidenceItems = pgTable(
40+ 'civic_evidence_items',
41+ {
42+ id: bigserial('id', { mode: 'number' }).primaryKey(),
43+ civicId: integer('civic_id').notNull(),
44+ name: text('name'),
45+ molecularProfileId: integer('molecular_profile_id'),
46+ molecularProfileName: text('molecular_profile_name'),
47+ geneSymbols: text('gene_symbols').array().notNull().default([]),
48+ geneIds: text('gene_ids').array().notNull().default([]),
49+ variantIds: text('variant_ids').array().notNull().default([]),
50+ civicVariantIds: integer('civic_variant_ids').array().notNull().default([]),
51+ diseaseName: text('disease_name'),
52+ doid: text('doid'),
53+ cancerId: ciId('cancer_id'),
54+ cancerMatchType: text('cancer_match_type'),
55+ therapyNames: text('therapy_names').array().notNull().default([]),
56+ therapyIds: text('therapy_ids').array().notNull().default([]), // CI-DRUG-…
57+ therapyInteractionType: text('therapy_interaction_type'),
58+ evidenceType: text('evidence_type'), // PREDICTIVE | PROGNOSTIC | DIAGNOSTIC | PREDISPOSING | ONCOGENIC | FUNCTIONAL
59+ evidenceLevel: text('evidence_level'), // A-E
60+ evidenceDirection: text('evidence_direction'), // SUPPORTS | DOES_NOT_SUPPORT
61+ significance: text('significance'), // SENSITIVITYRESPONSE | RESISTANCE | …
62+ evidenceRating: integer('evidence_rating'),
63+ status: text('status'), // ACCEPTED | SUBMITTED | REJECTED
64+ description: text('description'),
65+ pmid: text('pmid'),
66+ sourceCitation: text('source_citation'),
67+ phenotypes: text('phenotypes').array().notNull().default([]),
68+ provenanceId: integer('provenance_id').notNull(),
69+ ingestRunId: text('ingest_run_id'),
70+ updatedAt: updatedAt(),
71+ },
72+ (t) => [uniqueIndex('civic_evidence_uq').on(t.civicId), index('civic_evidence_cancer_idx').on(t.cancerId), index('civic_evidence_gene_idx').on(t.geneSymbols)],
73+);
74+
75+/** Risk factors / carcinogens as first-class entities (CLAUDE.md §74, §133). */
76+export const riskFactors = pgTable(
77+ 'risk_factors',
78+ {
79+ id: bigserial('id', { mode: 'number' }).primaryKey(),
80+ slug: text('slug').notNull(),
81+ name: text('name').notNull(),
82+ kind: text('kind').notNull(), // behavioral | infectious | environmental | occupational | genetic | hormonal | demographic | carcinogen
83+ classificationAuthority: text('classification_authority'),
84+ classification: text('classification'),
85+ description: text('description'),
86+ createdAt: createdAt(),
87+ },
88+ (t) => [uniqueIndex('risk_factors_slug_uq').on(t.slug)],
89+);
added packages/database/src/schema/genomics.ts +184 −0
@@ -0,0 +1,184 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, real, jsonb, boolean, timestamp } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Genes — HGNC is authoritative for symbols (CLAUDE.md §11, §143). */
5+export const genes = pgTable(
6+ 'genes',
7+ {
8+ id: ciId().primaryKey(), // CI-GENE-…
9+ hgncId: text('hgnc_id'), // HGNC:11998
10+ symbol: text('symbol').notNull(),
11+ name: text('name'),
12+ locusType: text('locus_type'),
13+ locusGroup: text('locus_group'),
14+ location: text('location'),
15+ chromosome: text('chromosome'),
16+ ensemblGeneId: text('ensembl_gene_id'),
17+ ncbiGeneId: text('ncbi_gene_id'),
18+ omimIds: text('omim_ids').array().notNull().default([]),
19+ uniprotIds: text('uniprot_ids').array().notNull().default([]),
20+ refseqAccession: text('refseq_accession'),
21+ prevSymbols: text('prev_symbols').array().notNull().default([]),
22+ aliasSymbols: text('alias_symbols').array().notNull().default([]),
23+ geneFamilies: text('gene_families').array().notNull().default([]),
24+ status: text('status').notNull().default('Approved'),
25+ isCancerGene: boolean('is_cancer_gene').notNull().default(false), // has ≥1 curated cancer edge (derived)
26+ civicGeneId: integer('civic_gene_id'),
27+ description: text('description'),
28+ createdAt: createdAt(),
29+ updatedAt: updatedAt(),
30+ },
31+ (t) => [uniqueIndex('genes_symbol_uq').on(t.symbol), uniqueIndex('genes_hgnc_uq').on(t.hgncId), index('genes_ensembl_idx').on(t.ensemblGeneId), index('genes_ncbi_idx').on(t.ncbiGeneId)],
32+);
33+
34+export const geneAliases = pgTable(
35+ 'gene_aliases',
36+ {
37+ id: bigserial('id', { mode: 'number' }).primaryKey(),
38+ geneId: ciId('gene_id').notNull(),
39+ alias: text('alias').notNull(),
40+ aliasType: text('alias_type').notNull(), // prev_symbol | alias_symbol | prev_name | alias_name
41+ sourceId: ciId('source_id'),
42+ },
43+ (t) => [uniqueIndex('gene_aliases_uq').on(t.geneId, t.alias, t.aliasType), index('gene_aliases_alias_idx').on(t.alias)],
44+);
45+
46+/** Variants (CLAUDE.md §235-239): coordinates always carry assembly; original + normalized kept. */
47+export const variants = pgTable(
48+ 'variants',
49+ {
50+ id: ciId().primaryKey(), // CI-VAR-…
51+ slug: text('slug').notNull(),
52+ geneId: ciId('gene_id'),
53+ geneSymbol: text('gene_symbol'),
54+ name: text('name').notNull(), // e.g. "V600E", "Exon 19 Deletion", "Amplification"
55+ variantType: text('variant_type'), // SO-style: SNV | MNV | insertion | deletion | indel | fusion | amplification | deletion_cna | loss_of_heterozygosity | promoter_mutation | splice | expression | epigenetic | structural | other
56+ hgvsG: text('hgvs_g'),
57+ hgvsC: text('hgvs_c'),
58+ hgvsP: text('hgvs_p'),
59+ assembly: text('assembly'), // GRCh37 | GRCh38
60+ chromosome: text('chromosome'),
61+ start: integer('start'),
62+ end: integer('end'),
63+ referenceBases: text('reference_bases'),
64+ alternateBases: text('alternate_bases'),
65+ coordinates: jsonb('coordinates').$type<Array<Record<string, unknown>>>().notNull().default([]), // per-assembly list
66+ clinvarVariationId: text('clinvar_variation_id'),
67+ civicVariantId: integer('civic_variant_id'),
68+ dbsnpIds: text('dbsnp_ids').array().notNull().default([]),
69+ fusionPartners: text('fusion_partners').array().notNull().default([]), // [5' gene, 3' gene]
70+ createdAt: createdAt(),
71+ updatedAt: updatedAt(),
72+ },
73+ (t) => [uniqueIndex('variants_slug_uq').on(t.slug), index('variants_gene_idx').on(t.geneId), index('variants_clinvar_idx').on(t.clinvarVariationId), index('variants_civic_idx').on(t.civicVariantId)],
74+);
75+
76+export const variantAliases = pgTable(
77+ 'variant_aliases',
78+ {
79+ id: bigserial('id', { mode: 'number' }).primaryKey(),
80+ variantId: ciId('variant_id').notNull(),
81+ alias: text('alias').notNull(),
82+ sourceId: ciId('source_id'),
83+ },
84+ (t) => [uniqueIndex('variant_aliases_uq').on(t.variantId, t.alias)],
85+);
86+
87+/** ClinVar interpretations (CLAUDE.md §10.7): structured, never flattened. */
88+export const variantClinicalSignificance = pgTable(
89+ 'variant_clinical_significance',
90+ {
91+ id: bigserial('id', { mode: 'number' }).primaryKey(),
92+ variantId: ciId('variant_id').notNull(),
93+ clinvarVariationId: text('clinvar_variation_id').notNull(),
94+ clinicalSignificance: text('clinical_significance').notNull(),
95+ reviewStatus: text('review_status'),
96+ starRating: integer('star_rating'),
97+ lastEvaluated: text('last_evaluated'),
98+ conditions: text('conditions').array().notNull().default([]),
99+ conditionCancerIds: text('condition_cancer_ids').array().notNull().default([]),
100+ originSimple: text('origin_simple'),
101+ numberSubmitters: integer('number_submitters'),
102+ provenanceId: integer('provenance_id').notNull(),
103+ ingestRunId: text('ingest_run_id'),
104+ updatedAt: updatedAt(),
105+ },
106+ (t) => [uniqueIndex('variant_clinsig_uq').on(t.clinvarVariationId)],
107+);
108+
109+export const biomarkers = pgTable(
110+ 'biomarkers',
111+ {
112+ id: ciId().primaryKey(), // CI-BIO-…
113+ slug: text('slug').notNull(),
114+ name: text('name').notNull(),
115+ kind: text('kind').notNull(), // gene_mutation | protein_expression | hormone_receptor | immune_marker | msi | tmb | hrd | ctdna | methylation | signature | cell_surface | other
116+ geneId: ciId('gene_id'),
117+ ncitCode: text('ncit_code'),
118+ description: text('description'),
119+ measurement: jsonb('measurement').$type<Record<string, unknown>>().notNull().default({}), // assay/clone/scoring/thresholds (§241-243)
120+ createdAt: createdAt(),
121+ updatedAt: updatedAt(),
122+ },
123+ (t) => [uniqueIndex('biomarkers_slug_uq').on(t.slug)],
124+);
125+
126+/** Genomic studies / cohorts (GDC projects, cBioPortal studies…) — original study IDs preserved. */
127+export const genomicCohorts = pgTable(
128+ 'genomic_cohorts',
129+ {
130+ id: ciId().primaryKey(), // CI-STUDY-…
131+ sourceId: ciId('source_id').notNull(),
132+ studyId: text('study_id').notNull(), // TCGA-PAAD
133+ name: text('name').notNull(),
134+ program: text('program'),
135+ primarySites: text('primary_sites').array().notNull().default([]),
136+ diseaseTypes: text('disease_types').array().notNull().default([]),
137+ cancerId: ciId('cancer_id'),
138+ cancerMatchType: text('cancer_match_type'),
139+ caseCount: integer('case_count'),
140+ casesWithSsm: integer('cases_with_ssm'), // denominator for mutation frequencies
141+ dataRelease: text('data_release'),
142+ accessLevel: text('access_level').notNull().default('open'),
143+ url: text('url'),
144+ provenanceId: integer('provenance_id'),
145+ updatedAt: updatedAt(),
146+ },
147+ (t) => [uniqueIndex('genomic_cohorts_uq').on(t.sourceId, t.studyId), index('genomic_cohorts_cancer_idx').on(t.cancerId)],
148+);
149+
150+/** Gene alteration frequency per cohort — denominator is mandatory (CLAUDE.md §261-262). */
151+export const cancerGeneFrequencies = pgTable(
152+ 'cancer_gene_frequencies',
153+ {
154+ id: bigserial('id', { mode: 'number' }).primaryKey(),
155+ cohortId: ciId('cohort_id').notNull(),
156+ cancerId: ciId('cancer_id'),
157+ geneId: ciId('gene_id'),
158+ geneSymbol: text('gene_symbol').notNull(),
159+ alterationType: text('alteration_type').notNull().default('ssm'), // ssm | cnv_gain | cnv_loss | fusion
160+ casesAffected: integer('cases_affected').notNull(),
161+ casesProfiled: integer('cases_profiled').notNull(),
162+ frequency: real('frequency').notNull(),
163+ rank: integer('rank'),
164+ dataRelease: text('data_release'),
165+ provenanceId: integer('provenance_id').notNull(),
166+ updatedAt: updatedAt(),
167+ },
168+ (t) => [uniqueIndex('cancer_gene_freq_uq').on(t.cohortId, t.geneSymbol, t.alterationType), index('cancer_gene_freq_cancer_idx').on(t.cancerId, t.frequency), index('cancer_gene_freq_gene_idx').on(t.geneId)],
169+);
170+
171+export const entityEmbeddings = pgTable(
172+ 'entity_embeddings',
173+ {
174+ id: bigserial('id', { mode: 'number' }).primaryKey(),
175+ entityType: text('entity_type').notNull(),
176+ entityId: text('entity_id').notNull(),
177+ model: text('model').notNull(),
178+ dimensions: integer('dimensions').notNull(),
179+ textHash: text('text_hash').notNull(),
180+ embedding: text('embedding'), // stored via raw SQL cast to vector(n); model recorded on the row
181+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
182+ },
183+ (t) => [uniqueIndex('entity_embeddings_uq').on(t.entityType, t.entityId, t.model)],
184+);
added packages/database/src/schema/index.ts +9 −0
@@ -0,0 +1,9 @@
1+export * from './sources.js';
2+export * from './cancers.js';
3+export * from './genomics.js';
4+export * from './drugs.js';
5+export * from './trials.js';
6+export * from './publications.js';
7+export * from './evidence.js';
8+export * from './epidemiology.js';
9+export * from './rankings.js';
added packages/database/src/schema/publications.ts +68 −0
@@ -0,0 +1,68 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, boolean, jsonb, real } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Publications (CLAUDE.md §10.6, §342): one entity per paper across PubMed/DOI/Europe PMC. */
5+export const publications = pgTable(
6+ 'publications',
7+ {
8+ id: ciId().primaryKey(), // CI-PUB-…
9+ pmid: text('pmid'),
10+ doi: text('doi'),
11+ pmcid: text('pmcid'),
12+ title: text('title').notNull(),
13+ abstract: text('abstract'), // stored only where NLM terms permit (abstract text may carry publisher copyright; we store it for indexing, display truncated with link)
14+ journal: text('journal'),
15+ journalIso: text('journal_iso'),
16+ pubDate: text('pub_date'),
17+ pubYear: integer('pub_year'),
18+ publicationTypes: text('publication_types').array().notNull().default([]),
19+ meshTerms: jsonb('mesh_terms').$type<Array<{ descriptor: string; ui?: string; major: boolean; qualifiers?: string[] }>>().notNull().default([]),
20+ authors: jsonb('authors').$type<Array<{ name: string; affiliation?: string; orcid?: string }>>().notNull().default([]),
21+ language: text('language'),
22+ isPreprint: boolean('is_preprint').notNull().default(false),
23+ retracted: boolean('retracted').notNull().default(false),
24+ retractionNotice: text('retraction_notice'),
25+ nctIds: text('nct_ids').array().notNull().default([]),
26+ citedByCount: integer('cited_by_count'),
27+ sourceRecordId: integer('source_record_id'),
28+ ingestRunId: text('ingest_run_id'),
29+ createdAt: createdAt(),
30+ updatedAt: updatedAt(),
31+ },
32+ (t) => [uniqueIndex('publications_pmid_uq').on(t.pmid), index('publications_doi_idx').on(t.doi), index('publications_year_idx').on(t.pubYear), index('publications_retracted_idx').on(t.retracted)],
33+);
34+
35+/** publication → entity links (CLAUDE.md §165-166, §271): extraction method + status preserved. */
36+export const publicationEntityEdges = pgTable(
37+ 'publication_entity_edges',
38+ {
39+ id: bigserial('id', { mode: 'number' }).primaryKey(),
40+ publicationId: ciId('publication_id').notNull(),
41+ entityType: text('entity_type').notNull(), // cancer | gene | variant | drug | biomarker | trial
42+ entityId: text('entity_id').notNull(),
43+ method: text('method').notNull(), // mesh | dictionary | registry_reference | civic_curation | ner | llm | curator
44+ confidence: real('confidence'),
45+ status: text('status').notNull().default('candidate'), // candidate | validated | rejected
46+ sourceId: ciId('source_id'),
47+ ingestRunId: text('ingest_run_id'),
48+ createdAt: createdAt(),
49+ },
50+ (t) => [uniqueIndex('pub_entity_edges_uq').on(t.publicationId, t.entityType, t.entityId, t.method), index('pub_entity_edges_entity_idx').on(t.entityType, t.entityId)],
51+);
52+
53+/** Research activity counts per cancer from PubMed queries — the formula (query string) is stored (CLAUDE.md §251). */
54+export const literatureCounts = pgTable(
55+ 'literature_counts',
56+ {
57+ id: bigserial('id', { mode: 'number' }).primaryKey(),
58+ cancerId: ciId('cancer_id').notNull(),
59+ windowKey: text('window_key').notNull(), // all | 12m | 5y | 10y | y2015 …
60+ windowStart: text('window_start'),
61+ windowEnd: text('window_end'),
62+ query: text('query').notNull(), // exact PubMed query used
63+ count: integer('count').notNull(),
64+ provenanceId: integer('provenance_id').notNull(),
65+ computedAt: updatedAt(),
66+ },
67+ (t) => [uniqueIndex('literature_counts_uq').on(t.cancerId, t.windowKey), index('literature_counts_window_idx').on(t.windowKey, t.count)],
68+);
added packages/database/src/schema/rankings.ts +138 −0
@@ -0,0 +1,138 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, real, jsonb, boolean, timestamp, doublePrecision } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Metric catalog (CLAUDE.md §250-251): every derived number has a versioned formula. */
5+export const metricDefinitions = pgTable(
6+ 'metric_definitions',
7+ {
8+ id: ciId().primaryKey(), // CI-METRIC-…
9+ slug: text('slug').notNull(), // active_trials | mortality_incidence_ratio | …
10+ name: text('name').notNull(),
11+ description: text('description').notNull(),
12+ formula: text('formula').notNull(),
13+ formulaVersion: text('formula_version').notNull(), // ci-mir-v1
14+ unit: text('unit').notNull(),
15+ higherIsWorse: boolean('higher_is_worse'),
16+ aggregation: text('aggregation'),
17+ validDimensions: text('valid_dimensions').array().notNull().default([]),
18+ sourceSlugs: text('source_slugs').array().notNull().default([]),
19+ category: text('category').notNull(), // burden | lethality | trend | rarity | treatment | trials | research | molecular | unmet_need | composite
20+ eligibility: jsonb('eligibility').$type<Record<string, unknown>>().notNull().default({}),
21+ experimental: boolean('experimental').notNull().default(false),
22+ createdAt: createdAt(),
23+ updatedAt: updatedAt(),
24+ },
25+ (t) => [uniqueIndex('metric_definitions_slug_uq').on(t.slug)],
26+);
27+
28+/** Ranking snapshot = one metric × one scope × one formula version at one time (CLAUDE.md §33, §184). */
29+export const rankingSnapshots = pgTable(
30+ 'ranking_snapshots',
31+ {
32+ id: bigserial('id', { mode: 'number' }).primaryKey(),
33+ metricId: ciId('metric_id').notNull(),
34+ metricSlug: text('metric_slug').notNull(),
35+ scopeKey: text('scope_key').notNull(), // e.g. "geo=world|sex=all|age=all|year=2022|level=top"
36+ geography: text('geography').notNull().default('WORLD'),
37+ sex: text('sex').notNull().default('all'),
38+ ageGroup: text('age_group').notNull().default('all'),
39+ year: integer('year'),
40+ entityLevel: text('entity_level').notNull().default('top'), // top | histology | subtype | rare | all
41+ formulaVersion: text('formula_version').notNull(),
42+ eligibleEntities: integer('eligible_entities').notNull(),
43+ inputsHash: text('inputs_hash').notNull(),
44+ sourceIds: text('source_ids').array().notNull().default([]),
45+ isCurrent: boolean('is_current').notNull().default(true),
46+ generatedAt: timestamp('generated_at', { withTimezone: true }).notNull().defaultNow(),
47+ },
48+ (t) => [index('ranking_snapshots_lookup_idx').on(t.metricSlug, t.scopeKey, t.isCurrent)],
49+);
50+
51+export const rankings = pgTable(
52+ 'rankings',
53+ {
54+ id: bigserial('id', { mode: 'number' }).primaryKey(),
55+ snapshotId: integer('snapshot_id').notNull(),
56+ metricSlug: text('metric_slug').notNull(),
57+ scopeKey: text('scope_key').notNull(),
58+ cancerId: ciId('cancer_id').notNull(),
59+ rank: integer('rank').notNull(),
60+ eligibleEntities: integer('eligible_entities').notNull(),
61+ percentile: real('percentile').notNull(),
62+ value: doublePrecision('value').notNull(),
63+ unit: text('unit').notNull(),
64+ confidence: text('confidence').notNull().default('MEDIUM'), // HIGH | MEDIUM | LOW | INSUFFICIENT_DATA
65+ inputs: jsonb('inputs').$type<Record<string, unknown>>().notNull().default({}), // lineage: observation ids, counts, formula inputs
66+ breakdown: jsonb('breakdown').$type<Record<string, number>>(), // composite components
67+ previousRank: integer('previous_rank'),
68+ generatedAt: timestamp('generated_at', { withTimezone: true }).notNull().defaultNow(),
69+ },
70+ (t) => [uniqueIndex('rankings_uq').on(t.snapshotId, t.cancerId), index('rankings_cancer_idx').on(t.cancerId, t.metricSlug), index('rankings_lookup_idx').on(t.metricSlug, t.scopeKey, t.rank)],
71+);
72+
73+/** Precomputed counters per entity, refreshed deterministically from canonical relations (CLAUDE.md §287). */
74+export const entityCounters = pgTable(
75+ 'entity_counters',
76+ {
77+ id: bigserial('id', { mode: 'number' }).primaryKey(),
78+ entityType: text('entity_type').notNull(),
79+ entityId: text('entity_id').notNull(),
80+ trialCount: integer('trial_count').notNull().default(0),
81+ activeTrialCount: integer('active_trial_count').notNull().default(0),
82+ recruitingTrialCount: integer('recruiting_trial_count').notNull().default(0),
83+ phase3TrialCount: integer('phase3_trial_count').notNull().default(0),
84+ publicationCount: integer('publication_count').notNull().default(0),
85+ publicationCount5y: integer('publication_count_5y').notNull().default(0),
86+ publicationCount12m: integer('publication_count_12m').notNull().default(0),
87+ geneCount: integer('gene_count').notNull().default(0),
88+ variantCount: integer('variant_count').notNull().default(0),
89+ drugCount: integer('drug_count').notNull().default(0),
90+ approvedDrugCount: integer('approved_drug_count').notNull().default(0),
91+ evidenceCount: integer('evidence_count').notNull().default(0),
92+ cohortCount: integer('cohort_count').notNull().default(0),
93+ subtypeCount: integer('subtype_count').notNull().default(0),
94+ descendantCount: integer('descendant_count').notNull().default(0),
95+ epidemiologyObsCount: integer('epidemiology_obs_count').notNull().default(0),
96+ survivalObsCount: integer('survival_obs_count').notNull().default(0),
97+ completeness: jsonb('completeness').$type<Record<string, number>>().notNull().default({}),
98+ computedAt: updatedAt(),
99+ },
100+ (t) => [uniqueIndex('entity_counters_uq').on(t.entityType, t.entityId), index('entity_counters_trials_idx').on(t.entityType, t.activeTrialCount)],
101+);
102+
103+/** Public API keys (CLAUDE.md §190-191). */
104+export const apiKeys = pgTable(
105+ 'api_keys',
106+ {
107+ id: bigserial('id', { mode: 'number' }).primaryKey(),
108+ keyHash: text('key_hash').notNull(),
109+ prefix: text('prefix').notNull(),
110+ label: text('label'),
111+ ownerEmail: text('owner_email'),
112+ tier: text('tier').notNull().default('free'), // free | research | pro | institutional
113+ rateLimitPerMinute: integer('rate_limit_per_minute').notNull().default(60),
114+ active: boolean('active').notNull().default(true),
115+ lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
116+ createdAt: createdAt(),
117+ },
118+ (t) => [uniqueIndex('api_keys_hash_uq').on(t.keyHash)],
119+);
120+
121+/** Cached AI syntheses (CLAUDE.md §44, §322) — model, prompt version and record snapshot recorded. */
122+export const aiAnswers = pgTable(
123+ 'ai_answers',
124+ {
125+ id: bigserial('id', { mode: 'number' }).primaryKey(),
126+ kind: text('kind').notNull(), // cancer_summary | ask
127+ subjectId: text('subject_id'),
128+ questionHash: text('question_hash').notNull(),
129+ question: text('question'),
130+ answer: jsonb('answer').$type<Record<string, unknown>>().notNull(),
131+ model: text('model').notNull(),
132+ promptVersion: text('prompt_version').notNull(),
133+ sourceSnapshot: jsonb('source_snapshot').$type<Record<string, unknown>>().notNull().default({}),
134+ dataAsOf: timestamp('data_as_of', { withTimezone: true }).notNull(),
135+ createdAt: createdAt(),
136+ },
137+ (t) => [uniqueIndex('ai_answers_uq').on(t.kind, t.questionHash, t.promptVersion)],
138+);
added packages/database/src/schema/sources.ts +228 −0
@@ -0,0 +1,228 @@
1+import { pgTable, text, integer, bigint, bigserial, boolean, jsonb, timestamp, index, uniqueIndex, real, varchar } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** Per-namespace counters for stable public IDs (never exposed, never reused). */
5+export const idSequences = pgTable('id_sequences', {
6+ namespace: varchar('namespace', { length: 16 }).primaryKey(),
7+ next: bigint('next', { mode: 'number' }).notNull().default(1),
8+});
9+
10+/** Source registry + license registry (CLAUDE.md §9, §95, §142). Seeded from connector manifests. */
11+export const sources = pgTable(
12+ 'sources',
13+ {
14+ id: ciId().primaryKey(), // CI-SOURCE-…
15+ slug: text('slug').notNull(), // connector id, e.g. "clinicaltrials"
16+ name: text('name').notNull(),
17+ organization: text('organization'),
18+ category: text('category').notNull(), // terminology | genomics | epidemiology | trials | literature | variants | drugs | regulatory
19+ description: text('description'),
20+ homepage: text('homepage'),
21+ docsUrl: text('docs_url'),
22+ termsUrl: text('terms_url'),
23+ accessType: text('access_type').notNull(), // api|bulk|rss|ftp|graphql|rest|scrape|manual
24+ accessAuth: text('access_auth').notNull(), // none|api_key|oauth|account|controlled
25+ license: text('license'),
26+ licenseStatus: text('license_status').notNull().default('review'), // review | approved | restricted | blocked
27+ commercialUse: text('commercial_use').notNull().default('unknown'), // allowed | restricted | prohibited | unknown
28+ redistribution: text('redistribution').notNull().default('unknown'), // allowed | attribution | restricted | prohibited | unknown
29+ attribution: text('attribution'),
30+ licenseReviewedAt: timestamp('license_reviewed_at', { withTimezone: true }),
31+ approvedForProduction: boolean('approved_for_production').notNull().default(false),
32+ updateFrequency: text('update_frequency'),
33+ supportsIncremental: boolean('supports_incremental').notNull().default(false),
34+ entities: text('entities').array().notNull().default([]),
35+ metrics: text('metrics').array().notNull().default([]),
36+ rateLimit: text('rate_limit'),
37+ status: text('status').notNull().default('planned'), // planned | active | paused | degraded | awaiting_credentials | review | retired
38+ tier: integer('tier').notNull().default(0),
39+ manifest: jsonb('manifest').$type<Record<string, unknown>>().notNull().default({}),
40+ createdAt: createdAt(),
41+ updatedAt: updatedAt(),
42+ },
43+ (t) => [uniqueIndex('sources_slug_uq').on(t.slug)],
44+);
45+
46+/** Every connector execution (CLAUDE.md §24, §176). */
47+export const ingestRuns = pgTable(
48+ 'ingest_runs',
49+ {
50+ id: text('id').primaryKey(), // ING-CLINICALTRIALS-20260908-000019
51+ connectorId: text('connector_id').notNull(),
52+ sourceId: ciId('source_id').notNull(),
53+ mode: text('mode').notNull().default('incremental'), // full | incremental | backfill | dry_run | probe
54+ status: text('status').notNull().default('running'), // running | succeeded | failed | partial | aborted
55+ startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
56+ finishedAt: timestamp('finished_at', { withTimezone: true }),
57+ durationMs: integer('duration_ms'),
58+ recordsFetched: integer('records_fetched').notNull().default(0),
59+ recordsCreated: integer('records_created').notNull().default(0),
60+ recordsUpdated: integer('records_updated').notNull().default(0),
61+ recordsUnchanged: integer('records_unchanged').notNull().default(0),
62+ recordsRejected: integer('records_rejected').notNull().default(0),
63+ httpRequests: integer('http_requests').notNull().default(0),
64+ httpFailures: integer('http_failures').notNull().default(0),
65+ rateLimitEvents: integer('rate_limit_events').notNull().default(0),
66+ validationFailures: integer('validation_failures').notNull().default(0),
67+ schemaDrift: jsonb('schema_drift').$type<unknown[]>().notNull().default([]),
68+ cursorBefore: jsonb('cursor_before'),
69+ cursorAfter: jsonb('cursor_after'),
70+ error: text('error'),
71+ log: jsonb('log').$type<Array<{ t: string; level: string; msg: string }>>().notNull().default([]),
72+ datasetVersion: text('dataset_version'),
73+ anomaly: text('anomaly'), // set when a destructive update was refused (CLAUDE.md §171)
74+ },
75+ (t) => [index('ingest_runs_connector_idx').on(t.connectorId, t.startedAt)],
76+);
77+
78+export const connectorCursors = pgTable('connector_cursors', {
79+ connectorId: text('connector_id').primaryKey(),
80+ cursor: jsonb('cursor').$type<Record<string, unknown>>().notNull().default({}),
81+ lastSuccessAt: timestamp('last_success_at', { withTimezone: true }),
82+ lastAttemptAt: timestamp('last_attempt_at', { withTimezone: true }),
83+ paused: boolean('paused').notNull().default(false),
84+ health: text('health').notNull().default('unknown'), // healthy | degraded | failing | review | awaiting_credentials | unknown
85+ healthDetail: text('health_detail'),
86+ updatedAt: updatedAt(),
87+});
88+
89+/** Schema-drift detection: observed field names/types per connector entity (CLAUDE.md §25). */
90+export const connectorFieldStats = pgTable(
91+ 'connector_field_stats',
92+ {
93+ id: bigserial('id', { mode: 'number' }).primaryKey(),
94+ connectorId: text('connector_id').notNull(),
95+ entity: text('entity').notNull(),
96+ field: text('field').notNull(),
97+ types: text('types').array().notNull().default([]),
98+ seenCount: integer('seen_count').notNull().default(0),
99+ nullCount: integer('null_count').notNull().default(0),
100+ firstSeenRun: text('first_seen_run'),
101+ lastSeenRun: text('last_seen_run'),
102+ updatedAt: updatedAt(),
103+ },
104+ (t) => [uniqueIndex('connector_field_stats_uq').on(t.connectorId, t.entity, t.field)],
105+);
106+
107+/** Source-native records (idempotency key = source + entity + source record id; CLAUDE.md §91-92). */
108+export const sourceRecords = pgTable(
109+ 'source_records',
110+ {
111+ id: bigserial('id', { mode: 'number' }).primaryKey(),
112+ sourceId: ciId('source_id').notNull(),
113+ entityKind: text('entity_kind').notNull(),
114+ sourceRecordId: text('source_record_id').notNull(),
115+ payloadHash: text('payload_hash').notNull(),
116+ rawPath: text('raw_path'), // data lake path /raw/{source}/{date}/{entity}/{id}.json.gz
117+ status: text('status').notNull().default('active'), // active | deprecated | retracted | withdrawn | source_missing
118+ firstSeenRun: text('first_seen_run'),
119+ lastSeenRun: text('last_seen_run'),
120+ retrievedAt: timestamp('retrieved_at', { withTimezone: true }).notNull().defaultNow(),
121+ sourceUpdatedAt: timestamp('source_updated_at', { withTimezone: true }),
122+ canonicalType: text('canonical_type'),
123+ canonicalId: text('canonical_id'),
124+ createdAt: createdAt(),
125+ updatedAt: updatedAt(),
126+ },
127+ (t) => [
128+ uniqueIndex('source_records_uq').on(t.sourceId, t.entityKind, t.sourceRecordId),
129+ index('source_records_canonical_idx').on(t.canonicalType, t.canonicalId),
130+ ],
131+);
132+
133+/** Provenance record (CLAUDE.md §2). Referenced by observations, edges, descriptions, rankings. */
134+export const provenance = pgTable(
135+ 'provenance',
136+ {
137+ id: bigserial('id', { mode: 'number' }).primaryKey(),
138+ publicId: ciId('public_id'), // CI-PROV-… (minted lazily when exposed)
139+ sourceId: ciId('source_id').notNull(),
140+ sourceRecordId: text('source_record_id'),
141+ sourceUrl: text('source_url'),
142+ dataset: text('dataset'),
143+ datasetVersion: text('dataset_version'),
144+ publicationId: ciId('publication_id'),
145+ pmid: text('pmid'),
146+ doi: text('doi'),
147+ retrievedAt: timestamp('retrieved_at', { withTimezone: true }).notNull(),
148+ publishedAt: text('published_at'),
149+ updatedAtSource: text('updated_at_source'),
150+ geography: text('geography'),
151+ population: text('population'),
152+ cohortSize: integer('cohort_size'),
153+ methodology: text('methodology'),
154+ evidenceType: text('evidence_type').notNull(),
155+ accessLevel: text('access_level').notNull().default('open'),
156+ confidence: real('confidence'),
157+ license: text('license'),
158+ ingestRunId: text('ingest_run_id'),
159+ createdAt: createdAt(),
160+ },
161+ (t) => [index('provenance_source_idx').on(t.sourceId, t.sourceRecordId), index('provenance_pmid_idx').on(t.pmid)],
162+);
163+
164+/** Unknown disease/drug labels are never discarded (CLAUDE.md §222). */
165+export const unresolvedLabels = pgTable(
166+ 'unresolved_labels',
167+ {
168+ id: bigserial('id', { mode: 'number' }).primaryKey(),
169+ sourceId: ciId('source_id').notNull(),
170+ entityKind: text('entity_kind').notNull(), // cancer | drug | gene | biomarker
171+ sourceText: text('source_text').notNull(),
172+ normalized: text('normalized').notNull(),
173+ context: jsonb('context').$type<Record<string, unknown>>().notNull().default({}),
174+ count: integer('count').notNull().default(1),
175+ status: text('status').notNull().default('open'), // open | mapped | rejected | ignored
176+ suggestedId: text('suggested_id'),
177+ suggestedMatchType: text('suggested_match_type'),
178+ suggestedScore: real('suggested_score'),
179+ resolvedId: text('resolved_id'),
180+ resolvedBy: text('resolved_by'),
181+ createdAt: createdAt(),
182+ updatedAt: updatedAt(),
183+ },
184+ (t) => [uniqueIndex('unresolved_labels_uq').on(t.sourceId, t.entityKind, t.normalized), index('unresolved_labels_count_idx').on(t.status, t.count)],
185+);
186+
187+/** Entity change history (CLAUDE.md §96). */
188+export const changeEvents = pgTable(
189+ 'change_events',
190+ {
191+ id: bigserial('id', { mode: 'number' }).primaryKey(),
192+ entityType: text('entity_type').notNull(),
193+ entityId: text('entity_id').notNull(),
194+ kind: text('kind').notNull(), // created | updated | trial_added | approval_added | ranking_changed | merged | deprecated …
195+ summary: text('summary').notNull(),
196+ before: jsonb('before'),
197+ after: jsonb('after'),
198+ ingestRunId: text('ingest_run_id'),
199+ createdAt: createdAt(),
200+ },
201+ (t) => [index('change_events_entity_idx').on(t.entityType, t.entityId, t.createdAt)],
202+);
203+
204+/** Curator/admin audit log (CLAUDE.md §348). */
205+export const auditLog = pgTable('audit_log', {
206+ id: bigserial('id', { mode: 'number' }).primaryKey(),
207+ actor: text('actor').notNull(),
208+ action: text('action').notNull(),
209+ entityType: text('entity_type'),
210+ entityId: text('entity_id'),
211+ before: jsonb('before'),
212+ after: jsonb('after'),
213+ reason: text('reason'),
214+ createdAt: createdAt(),
215+});
216+
217+/** Entity merge queue + audit (CLAUDE.md §70). Reversible. */
218+export const entityMerges = pgTable('entity_merges', {
219+ id: bigserial('id', { mode: 'number' }).primaryKey(),
220+ entityType: text('entity_type').notNull(),
221+ keepId: text('keep_id').notNull(),
222+ mergeId: text('merge_id').notNull(),
223+ evidence: jsonb('evidence').$type<Record<string, unknown>>().notNull().default({}),
224+ status: text('status').notNull().default('proposed'), // proposed | merged | kept_separate | reverted
225+ decidedBy: text('decided_by'),
226+ decidedAt: timestamp('decided_at', { withTimezone: true }),
227+ createdAt: createdAt(),
228+});
added packages/database/src/schema/trials.ts +115 −0
@@ -0,0 +1,115 @@
1+import { pgTable, text, integer, bigserial, index, uniqueIndex, boolean, jsonb, real, date } from 'drizzle-orm/pg-core';
2+import { ciId, createdAt, updatedAt } from './_common.js';
3+
4+/** ClinicalTrials.gov studies (CLAUDE.md §10.5). */
5+export const clinicalTrials = pgTable(
6+ 'clinical_trials',
7+ {
8+ id: ciId().primaryKey(), // CI-TRIAL-…
9+ nctId: text('nct_id').notNull(),
10+ briefTitle: text('brief_title').notNull(),
11+ officialTitle: text('official_title'),
12+ acronym: text('acronym'),
13+ studyType: text('study_type'), // INTERVENTIONAL | OBSERVATIONAL | EXPANDED_ACCESS
14+ phases: text('phases').array().notNull().default([]), // EARLY_PHASE1 | PHASE1 | PHASE2 | PHASE3 | PHASE4 | NA
15+ overallStatus: text('overall_status'), // RECRUITING | ACTIVE_NOT_RECRUITING | COMPLETED | …
16+ whyStopped: text('why_stopped'),
17+ startDate: text('start_date'),
18+ primaryCompletionDate: text('primary_completion_date'),
19+ completionDate: text('completion_date'),
20+ firstPostedDate: text('first_posted_date'),
21+ lastUpdatePostedDate: text('last_update_posted_date'),
22+ resultsFirstPostedDate: text('results_first_posted_date'),
23+ hasResults: boolean('has_results').notNull().default(false),
24+ enrollmentCount: integer('enrollment_count'),
25+ enrollmentType: text('enrollment_type'),
26+ leadSponsor: text('lead_sponsor'),
27+ leadSponsorClass: text('lead_sponsor_class'),
28+ collaborators: text('collaborators').array().notNull().default([]),
29+ conditions: text('conditions').array().notNull().default([]), // raw free text
30+ keywords: text('keywords').array().notNull().default([]),
31+ interventions: jsonb('interventions').$type<Array<{ type: string; name: string; description?: string; otherNames?: string[] }>>().notNull().default([]),
32+ arms: jsonb('arms').$type<Array<Record<string, unknown>>>().notNull().default([]),
33+ primaryOutcomes: jsonb('primary_outcomes').$type<Array<Record<string, unknown>>>().notNull().default([]),
34+ secondaryOutcomes: jsonb('secondary_outcomes').$type<Array<Record<string, unknown>>>().notNull().default([]),
35+ eligibility: jsonb('eligibility').$type<Record<string, unknown>>().notNull().default({}),
36+ sex: text('sex'),
37+ minimumAge: text('minimum_age'),
38+ maximumAge: text('maximum_age'),
39+ countries: text('countries').array().notNull().default([]),
40+ locationsCount: integer('locations_count').notNull().default(0),
41+ references: jsonb('references').$type<Array<{ pmid?: string; type?: string; citation?: string }>>().notNull().default([]),
42+ briefSummary: text('brief_summary'),
43+ isOncology: boolean('is_oncology').notNull().default(true),
44+ sourceRecordId: integer('source_record_id'),
45+ ingestRunId: text('ingest_run_id'),
46+ createdAt: createdAt(),
47+ updatedAt: updatedAt(),
48+ },
49+ (t) => [
50+ uniqueIndex('clinical_trials_nct_uq').on(t.nctId),
51+ index('clinical_trials_status_idx').on(t.overallStatus),
52+ index('clinical_trials_updated_idx').on(t.lastUpdatePostedDate),
53+ index('clinical_trials_sponsor_idx').on(t.leadSponsor),
54+ ],
55+);
56+
57+/** trial → cancer mapping from free-text conditions (CLAUDE.md §10.5, §221). */
58+export const trialConditions = pgTable(
59+ 'trial_conditions',
60+ {
61+ id: bigserial('id', { mode: 'number' }).primaryKey(),
62+ trialId: ciId('trial_id').notNull(),
63+ conditionText: text('condition_text').notNull(),
64+ normalized: text('normalized').notNull(),
65+ cancerId: ciId('cancer_id'),
66+ matchType: text('match_type').notNull().default('UNRESOLVED'),
67+ confidence: real('confidence'),
68+ },
69+ (t) => [uniqueIndex('trial_conditions_uq').on(t.trialId, t.normalized), index('trial_conditions_cancer_idx').on(t.cancerId), index('trial_conditions_norm_idx').on(t.normalized)],
70+);
71+
72+export const trialInterventions = pgTable(
73+ 'trial_interventions',
74+ {
75+ id: bigserial('id', { mode: 'number' }).primaryKey(),
76+ trialId: ciId('trial_id').notNull(),
77+ name: text('name').notNull(),
78+ normalized: text('normalized').notNull(),
79+ interventionType: text('intervention_type'), // DRUG | BIOLOGICAL | DEVICE | PROCEDURE | RADIATION | BEHAVIORAL | OTHER
80+ drugId: ciId('drug_id'),
81+ matchType: text('match_type').notNull().default('UNRESOLVED'),
82+ },
83+ (t) => [uniqueIndex('trial_interventions_uq').on(t.trialId, t.normalized), index('trial_interventions_drug_idx').on(t.drugId)],
84+);
85+
86+export const trialLocations = pgTable(
87+ 'trial_locations',
88+ {
89+ id: bigserial('id', { mode: 'number' }).primaryKey(),
90+ trialId: ciId('trial_id').notNull(),
91+ facility: text('facility'),
92+ city: text('city'),
93+ state: text('state'),
94+ zip: text('zip'),
95+ country: text('country'),
96+ status: text('status'),
97+ lat: real('lat'),
98+ lng: real('lng'),
99+ },
100+ (t) => [index('trial_locations_trial_idx').on(t.trialId), index('trial_locations_country_idx').on(t.country)],
101+);
102+
103+/** Daily trial-creation pulse (derived; CLAUDE.md §332). */
104+export const trialPulse = pgTable(
105+ 'trial_pulse',
106+ {
107+ id: bigserial('id', { mode: 'number' }).primaryKey(),
108+ day: date('day').notNull(),
109+ cancerId: ciId('cancer_id'),
110+ phase: text('phase'),
111+ newTrials: integer('new_trials').notNull(),
112+ computedAt: updatedAt(),
113+ },
114+ (t) => [uniqueIndex('trial_pulse_uq').on(t.day, t.cancerId, t.phase)],
115+);
added packages/database/src/seed-data/geographies.ts +101 −0
@@ -0,0 +1,101 @@
1+/** Canonical geography seed (CLAUDE.md §117). ISO 3166-1; WHO regions per WHO membership lists. */
2+export interface GeographySeed {
3+ slug: string;
4+ name: string;
5+ kind: 'world' | 'region' | 'who_region' | 'country' | 'subdivision';
6+ iso2?: string;
7+ iso3?: string;
8+ parentSlug?: string;
9+ whoRegion?: string;
10+}
11+
12+const WHO_REGIONS: Array<[string, string]> = [
13+ ['who-afro', 'WHO African Region'],
14+ ['who-amro', 'WHO Region of the Americas'],
15+ ['who-searo', 'WHO South-East Asia Region'],
16+ ['who-euro', 'WHO European Region'],
17+ ['who-emro', 'WHO Eastern Mediterranean Region'],
18+ ['who-wpro', 'WHO Western Pacific Region'],
19+];
20+
21+// [iso2, iso3, name, whoRegion]
22+const COUNTRIES: Array<[string, string, string, string]> = [
23+ ['US', 'USA', 'United States', 'who-amro'],
24+ ['CA', 'CAN', 'Canada', 'who-amro'],
25+ ['MX', 'MEX', 'Mexico', 'who-amro'],
26+ ['BR', 'BRA', 'Brazil', 'who-amro'],
27+ ['AR', 'ARG', 'Argentina', 'who-amro'],
28+ ['CL', 'CHL', 'Chile', 'who-amro'],
29+ ['CO', 'COL', 'Colombia', 'who-amro'],
30+ ['PE', 'PER', 'Peru', 'who-amro'],
31+ ['CU', 'CUB', 'Cuba', 'who-amro'],
32+ ['GB', 'GBR', 'United Kingdom', 'who-euro'],
33+ ['IE', 'IRL', 'Ireland', 'who-euro'],
34+ ['FR', 'FRA', 'France', 'who-euro'],
35+ ['DE', 'DEU', 'Germany', 'who-euro'],
36+ ['IT', 'ITA', 'Italy', 'who-euro'],
37+ ['ES', 'ESP', 'Spain', 'who-euro'],
38+ ['PT', 'PRT', 'Portugal', 'who-euro'],
39+ ['NL', 'NLD', 'Netherlands', 'who-euro'],
40+ ['BE', 'BEL', 'Belgium', 'who-euro'],
41+ ['CH', 'CHE', 'Switzerland', 'who-euro'],
42+ ['AT', 'AUT', 'Austria', 'who-euro'],
43+ ['SE', 'SWE', 'Sweden', 'who-euro'],
44+ ['NO', 'NOR', 'Norway', 'who-euro'],
45+ ['DK', 'DNK', 'Denmark', 'who-euro'],
46+ ['FI', 'FIN', 'Finland', 'who-euro'],
47+ ['IS', 'ISL', 'Iceland', 'who-euro'],
48+ ['PL', 'POL', 'Poland', 'who-euro'],
49+ ['CZ', 'CZE', 'Czechia', 'who-euro'],
50+ ['HU', 'HUN', 'Hungary', 'who-euro'],
51+ ['GR', 'GRC', 'Greece', 'who-euro'],
52+ ['RO', 'ROU', 'Romania', 'who-euro'],
53+ ['UA', 'UKR', 'Ukraine', 'who-euro'],
54+ ['RU', 'RUS', 'Russian Federation', 'who-euro'],
55+ ['TR', 'TUR', 'Türkiye', 'who-euro'],
56+ ['IL', 'ISR', 'Israel', 'who-euro'],
57+ ['EG', 'EGY', 'Egypt', 'who-emro'],
58+ ['SA', 'SAU', 'Saudi Arabia', 'who-emro'],
59+ ['IR', 'IRN', 'Iran', 'who-emro'],
60+ ['PK', 'PAK', 'Pakistan', 'who-emro'],
61+ ['MA', 'MAR', 'Morocco', 'who-emro'],
62+ ['NG', 'NGA', 'Nigeria', 'who-afro'],
63+ ['ZA', 'ZAF', 'South Africa', 'who-afro'],
64+ ['KE', 'KEN', 'Kenya', 'who-afro'],
65+ ['ET', 'ETH', 'Ethiopia', 'who-afro'],
66+ ['GH', 'GHA', 'Ghana', 'who-afro'],
67+ ['UG', 'UGA', 'Uganda', 'who-afro'],
68+ ['TZ', 'TZA', 'Tanzania', 'who-afro'],
69+ ['DZ', 'DZA', 'Algeria', 'who-afro'],
70+ ['IN', 'IND', 'India', 'who-searo'],
71+ ['BD', 'BGD', 'Bangladesh', 'who-searo'],
72+ ['ID', 'IDN', 'Indonesia', 'who-searo'],
73+ ['TH', 'THA', 'Thailand', 'who-searo'],
74+ ['NP', 'NPL', 'Nepal', 'who-searo'],
75+ ['LK', 'LKA', 'Sri Lanka', 'who-searo'],
76+ ['CN', 'CHN', 'China', 'who-wpro'],
77+ ['JP', 'JPN', 'Japan', 'who-wpro'],
78+ ['KR', 'KOR', 'Republic of Korea', 'who-wpro'],
79+ ['AU', 'AUS', 'Australia', 'who-wpro'],
80+ ['NZ', 'NZL', 'New Zealand', 'who-wpro'],
81+ ['SG', 'SGP', 'Singapore', 'who-wpro'],
82+ ['MY', 'MYS', 'Malaysia', 'who-wpro'],
83+ ['PH', 'PHL', 'Philippines', 'who-wpro'],
84+ ['VN', 'VNM', 'Viet Nam', 'who-wpro'],
85+];
86+
87+const US_STATES: Array<[string, string]> = [
88+ ['AL', 'Alabama'], ['AK', 'Alaska'], ['AZ', 'Arizona'], ['AR', 'Arkansas'], ['CA', 'California'], ['CO', 'Colorado'], ['CT', 'Connecticut'], ['DE', 'Delaware'], ['DC', 'District of Columbia'], ['FL', 'Florida'], ['GA', 'Georgia'], ['HI', 'Hawaii'], ['ID', 'Idaho'], ['IL', 'Illinois'], ['IN', 'Indiana'], ['IA', 'Iowa'], ['KS', 'Kansas'], ['KY', 'Kentucky'], ['LA', 'Louisiana'], ['ME', 'Maine'], ['MD', 'Maryland'], ['MA', 'Massachusetts'], ['MI', 'Michigan'], ['MN', 'Minnesota'], ['MS', 'Mississippi'], ['MO', 'Missouri'], ['MT', 'Montana'], ['NE', 'Nebraska'], ['NV', 'Nevada'], ['NH', 'New Hampshire'], ['NJ', 'New Jersey'], ['NM', 'New Mexico'], ['NY', 'New York'], ['NC', 'North Carolina'], ['ND', 'North Dakota'], ['OH', 'Ohio'], ['OK', 'Oklahoma'], ['OR', 'Oregon'], ['PA', 'Pennsylvania'], ['RI', 'Rhode Island'], ['SC', 'South Carolina'], ['SD', 'South Dakota'], ['TN', 'Tennessee'], ['TX', 'Texas'], ['UT', 'Utah'], ['VT', 'Vermont'], ['VA', 'Virginia'], ['WA', 'Washington'], ['WV', 'West Virginia'], ['WI', 'Wisconsin'], ['WY', 'Wyoming'],
89+];
90+
91+const CA_PROVINCES: Array<[string, string]> = [
92+ ['AB', 'Alberta'], ['BC', 'British Columbia'], ['MB', 'Manitoba'], ['NB', 'New Brunswick'], ['NL', 'Newfoundland and Labrador'], ['NS', 'Nova Scotia'], ['NT', 'Northwest Territories'], ['NU', 'Nunavut'], ['ON', 'Ontario'], ['PE', 'Prince Edward Island'], ['QC', 'Quebec'], ['SK', 'Saskatchewan'], ['YT', 'Yukon'],
93+];
94+
95+export const GEOGRAPHY_SEED: GeographySeed[] = [
96+ { slug: 'world', name: 'World', kind: 'world' },
97+ ...WHO_REGIONS.map(([slug, name]) => ({ slug, name, kind: 'who_region' as const, parentSlug: 'world' })),
98+ ...COUNTRIES.map(([iso2, iso3, name, who]) => ({ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, '-'), name, kind: 'country' as const, iso2, iso3, parentSlug: who, whoRegion: who })),
99+ ...US_STATES.map(([code, name]) => ({ slug: `us-${code.toLowerCase()}`, name, kind: 'subdivision' as const, iso2: `US-${code}`, parentSlug: 'united-states', whoRegion: 'who-amro' })),
100+ ...CA_PROVINCES.map(([code, name]) => ({ slug: `ca-${code.toLowerCase()}`, name, kind: 'subdivision' as const, iso2: `CA-${code}`, parentSlug: 'canada', whoRegion: 'who-amro' })),
101+];
added packages/database/src/seed-data/metrics.ts +285 −0
@@ -0,0 +1,285 @@
1+/**
2+ * Metric catalog (CLAUDE.md §250-252, §352). Every derived number shown on the site references
3+ * one of these definitions by slug; the formula text and version are displayed on /methodology.
4+ *
5+ * Composite scores (CI-IMPACT etc.) are intentionally absent from Phase 1 (§353).
6+ */
7+export interface MetricSeed {
8+ slug: string;
9+ name: string;
10+ description: string;
11+ formula: string;
12+ formulaVersion: string;
13+ unit: string;
14+ higherIsWorse: boolean | null;
15+ aggregation: string | null;
16+ validDimensions: string[];
17+ sourceSlugs: string[];
18+ category: string;
19+ eligibility: Record<string, unknown>;
20+ experimental: boolean;
21+}
22+
23+export const METRIC_CATALOG: MetricSeed[] = [
24+ // --- Burden (epidemiology observations) ---
25+ {
26+ slug: 'incidence_count',
27+ name: 'Annual new cases',
28+ description: 'Number of new cases diagnosed in the reference year for the population scope. Registry counts are observed; national/global figures may be estimates and are labeled as such.',
29+ formula: 'epidemiology_observations.value WHERE metric = incidence_count',
30+ formulaVersion: 'ci-incidence-count-v1',
31+ unit: 'count',
32+ higherIsWorse: true,
33+ aggregation: 'none',
34+ validDimensions: ['geography', 'sex', 'age_group', 'year'],
35+ sourceSlugs: ['cdc-wonder', 'seer', 'iarc-globocan'],
36+ category: 'burden',
37+ eligibility: { requires: ['incidence_count'], entityLevel: 'top' },
38+ experimental: false,
39+ },
40+ {
41+ slug: 'mortality_count',
42+ name: 'Annual deaths',
43+ description: 'Number of deaths with this cancer as underlying cause in the reference year for the population scope.',
44+ formula: 'epidemiology_observations.value WHERE metric = mortality_count',
45+ formulaVersion: 'ci-mortality-count-v1',
46+ unit: 'count',
47+ higherIsWorse: true,
48+ aggregation: 'none',
49+ validDimensions: ['geography', 'sex', 'age_group', 'year'],
50+ sourceSlugs: ['cdc-wonder', 'seer', 'iarc-globocan'],
51+ category: 'burden',
52+ eligibility: { requires: ['mortality_count'], entityLevel: 'top' },
53+ experimental: false,
54+ },
55+ {
56+ slug: 'as_incidence_rate',
57+ name: 'Age-standardized incidence rate',
58+ description: 'Incidence per 100,000 person-years, age-standardized to the standard population declared by the source. Rates standardized to different standard populations are never compared in one ranking.',
59+ formula: 'epidemiology_observations.value WHERE metric = as_incidence_rate (standard_population fixed per scope)',
60+ formulaVersion: 'ci-asir-v1',
61+ unit: 'per_100k',
62+ higherIsWorse: true,
63+ aggregation: 'none',
64+ validDimensions: ['geography', 'sex', 'year'],
65+ sourceSlugs: ['cdc-wonder', 'seer', 'iarc-globocan'],
66+ category: 'burden',
67+ eligibility: { requires: ['as_incidence_rate'], entityLevel: 'top' },
68+ experimental: false,
69+ },
70+ {
71+ slug: 'as_mortality_rate',
72+ name: 'Age-standardized mortality rate',
73+ description: 'Deaths per 100,000 person-years, age-standardized to the standard population declared by the source.',
74+ formula: 'epidemiology_observations.value WHERE metric = as_mortality_rate (standard_population fixed per scope)',
75+ formulaVersion: 'ci-asmr-v1',
76+ unit: 'per_100k',
77+ higherIsWorse: true,
78+ aggregation: 'none',
79+ validDimensions: ['geography', 'sex', 'year'],
80+ sourceSlugs: ['cdc-wonder', 'seer', 'iarc-globocan'],
81+ category: 'burden',
82+ eligibility: { requires: ['as_mortality_rate'], entityLevel: 'top' },
83+ experimental: false,
84+ },
85+ // --- Lethality ---
86+ {
87+ slug: 'mortality_incidence_ratio',
88+ name: 'Mortality-to-incidence ratio',
89+ description: 'Deaths divided by new cases in the same population, year and sex. A crude proxy of lethality; it is not a survival probability and is affected by incidence trends and registration completeness.',
90+ formula: 'mortality_count / incidence_count (same geography, year, sex, source family)',
91+ formulaVersion: 'ci-mir-v1',
92+ unit: 'ratio',
93+ higherIsWorse: true,
94+ aggregation: 'none',
95+ validDimensions: ['geography', 'sex', 'year'],
96+ sourceSlugs: ['cdc-wonder', 'seer', 'iarc-globocan'],
97+ category: 'lethality',
98+ eligibility: { requires: ['incidence_count', 'mortality_count'], minIncidence: 100, entityLevel: 'top' },
99+ experimental: false,
100+ },
101+ {
102+ slug: 'five_year_survival',
103+ name: '5-year relative survival',
104+ description: '5-year relative survival for the diagnosis period and population declared by the source. Population survival statistics do not predict an individual outcome.',
105+ formula: 'survival_observations.probability WHERE survival_type = relative AND duration_months = 60 AND stage IS NULL',
106+ formulaVersion: 'ci-5ys-v1',
107+ unit: 'probability',
108+ higherIsWorse: false,
109+ aggregation: 'none',
110+ validDimensions: ['geography', 'sex', 'diagnosis_period'],
111+ sourceSlugs: ['seer'],
112+ category: 'lethality',
113+ eligibility: { requires: ['five_year_relative_survival'], minCohort: 50, entityLevel: 'top' },
114+ experimental: false,
115+ },
116+ // --- Clinical research (ClinicalTrials.gov) ---
117+ {
118+ slug: 'active_trials',
119+ name: 'Active clinical trials',
120+ description: 'Interventional studies registered on ClinicalTrials.gov whose overall status is RECRUITING, NOT_YET_RECRUITING, ENROLLING_BY_INVITATION or ACTIVE_NOT_RECRUITING and whose conditions map to this cancer or one of its descendants in the NCIt hierarchy.',
121+ formula: 'COUNT(DISTINCT trial) FROM trial_conditions JOIN clinical_trials WHERE cancer_id IN descendants(cancer) AND study_type = INTERVENTIONAL AND overall_status IN (RECRUITING, NOT_YET_RECRUITING, ENROLLING_BY_INVITATION, ACTIVE_NOT_RECRUITING)',
122+ formulaVersion: 'ci-active-trials-v1',
123+ unit: 'count',
124+ higherIsWorse: false,
125+ aggregation: 'descendants',
126+ validDimensions: ['entity_level'],
127+ sourceSlugs: ['clinicaltrials'],
128+ category: 'trials',
129+ eligibility: { entityLevel: 'any' },
130+ experimental: false,
131+ },
132+ {
133+ slug: 'recruiting_trials',
134+ name: 'Recruiting clinical trials',
135+ description: 'Interventional studies currently RECRUITING whose conditions map to this cancer or its descendants.',
136+ formula: 'COUNT(DISTINCT trial) … overall_status = RECRUITING AND study_type = INTERVENTIONAL',
137+ formulaVersion: 'ci-recruiting-trials-v1',
138+ unit: 'count',
139+ higherIsWorse: false,
140+ aggregation: 'descendants',
141+ validDimensions: ['entity_level'],
142+ sourceSlugs: ['clinicaltrials'],
143+ category: 'trials',
144+ eligibility: { entityLevel: 'any' },
145+ experimental: false,
146+ },
147+ {
148+ slug: 'phase3_trials',
149+ name: 'Active Phase III trials',
150+ description: 'Active interventional studies with PHASE3 among their phases, mapped to this cancer or its descendants.',
151+ formula: 'COUNT(DISTINCT trial) … active statuses AND PHASE3 = ANY(phases)',
152+ formulaVersion: 'ci-phase3-trials-v1',
153+ unit: 'count',
154+ higherIsWorse: false,
155+ aggregation: 'descendants',
156+ validDimensions: ['entity_level'],
157+ sourceSlugs: ['clinicaltrials'],
158+ category: 'trials',
159+ eligibility: { entityLevel: 'any' },
160+ experimental: false,
161+ },
162+ // --- Research activity (PubMed) ---
163+ {
164+ slug: 'publications_5y',
165+ name: 'Publications, last 5 years',
166+ description: 'PubMed records matching the cancer’s MeSH-anchored query (stored verbatim with the count) with a publication date in the last 5 full years plus the current year.',
167+ formula: 'literature_counts.count WHERE window_key = 5y (query stored per row)',
168+ formulaVersion: 'ci-pubs-5y-v1',
169+ unit: 'count',
170+ higherIsWorse: false,
171+ aggregation: 'none',
172+ validDimensions: ['entity_level'],
173+ sourceSlugs: ['pubmed'],
174+ category: 'research',
175+ eligibility: { entityLevel: 'any', requiresQuery: true },
176+ experimental: false,
177+ },
178+ {
179+ slug: 'publications_12m',
180+ name: 'Publications, last 12 months',
181+ description: 'PubMed records matching the cancer’s query with a publication date in the last 12 months.',
182+ formula: 'literature_counts.count WHERE window_key = 12m',
183+ formulaVersion: 'ci-pubs-12m-v1',
184+ unit: 'count',
185+ higherIsWorse: false,
186+ aggregation: 'none',
187+ validDimensions: ['entity_level'],
188+ sourceSlugs: ['pubmed'],
189+ category: 'research',
190+ eligibility: { entityLevel: 'any', requiresQuery: true },
191+ experimental: false,
192+ },
193+ {
194+ slug: 'publication_growth',
195+ name: 'Publication growth',
196+ description: 'Ratio of publications in the last 12 months to the average annual publications over the preceding 5-year window. Values above 1 indicate accelerating literature.',
197+ formula: 'publications_12m / (publications_5y_prior / 5)',
198+ formulaVersion: 'ci-pub-growth-v1',
199+ unit: 'ratio',
200+ higherIsWorse: null,
201+ aggregation: 'none',
202+ validDimensions: ['entity_level'],
203+ sourceSlugs: ['pubmed'],
204+ category: 'trend',
205+ eligibility: { entityLevel: 'any', minPublications5y: 50 },
206+ experimental: false,
207+ },
208+ // --- Molecular knowledge ---
209+ {
210+ slug: 'curated_evidence_items',
211+ name: 'Curated clinical evidence items',
212+ description: 'Accepted CIViC evidence items whose disease maps to this cancer or its descendants.',
213+ formula: 'COUNT(civic_evidence_items) WHERE status = ACCEPTED AND cancer_id IN descendants(cancer)',
214+ formulaVersion: 'ci-civic-evidence-v1',
215+ unit: 'count',
216+ higherIsWorse: false,
217+ aggregation: 'descendants',
218+ validDimensions: ['entity_level'],
219+ sourceSlugs: ['civic'],
220+ category: 'molecular',
221+ eligibility: { entityLevel: 'any' },
222+ experimental: false,
223+ },
224+ {
225+ slug: 'associated_genes',
226+ name: 'Genes with curated or cohort evidence',
227+ description: 'Distinct genes linked to this cancer (or descendants) by an accepted CIViC evidence item or by a GDC cohort frequency of at least 5% with ≥ 20 affected cases.',
228+ formula: 'COUNT(DISTINCT gene) FROM (civic accepted evidence ∪ gdc frequency ≥ 0.05 AND cases_affected ≥ 20)',
229+ formulaVersion: 'ci-genes-v1',
230+ unit: 'count',
231+ higherIsWorse: false,
232+ aggregation: 'descendants',
233+ validDimensions: ['entity_level'],
234+ sourceSlugs: ['civic', 'gdc'],
235+ category: 'molecular',
236+ eligibility: { entityLevel: 'any' },
237+ experimental: false,
238+ },
239+ {
240+ slug: 'genomic_cohorts',
241+ name: 'Public genomic cohorts',
242+ description: 'Open-access genomic studies (GDC projects) whose disease maps to this cancer or its descendants.',
243+ formula: 'COUNT(genomic_cohorts) WHERE cancer_id IN descendants(cancer)',
244+ formulaVersion: 'ci-cohorts-v1',
245+ unit: 'count',
246+ higherIsWorse: false,
247+ aggregation: 'descendants',
248+ validDimensions: ['entity_level'],
249+ sourceSlugs: ['gdc'],
250+ category: 'molecular',
251+ eligibility: { entityLevel: 'any' },
252+ experimental: false,
253+ },
254+ // --- Gap indexes (derived; require burden) ---
255+ {
256+ slug: 'trial_gap',
257+ name: 'Trial Gap Index',
258+ description: 'Burden percentile minus active-trial percentile within the same scope. Positive values flag cancers with high mortality burden but comparatively few active trials. A quantitative signal, not an accusation (CLAUDE.md §324).',
259+ formula: 'percentile(mortality_count) − percentile(active_trials)',
260+ formulaVersion: 'ci-trial-gap-v1',
261+ unit: 'percentile_points',
262+ higherIsWorse: true,
263+ aggregation: 'none',
264+ validDimensions: ['geography', 'year'],
265+ sourceSlugs: ['clinicaltrials', 'cdc-wonder', 'seer', 'iarc-globocan'],
266+ category: 'unmet_need',
267+ eligibility: { requires: ['mortality_count', 'active_trials'], entityLevel: 'top' },
268+ experimental: false,
269+ },
270+ {
271+ slug: 'research_gap',
272+ name: 'Research Gap Index',
273+ description: 'Burden percentile minus research-activity percentile (publications, last 5 years) within the same scope. Positive values flag cancers with high mortality burden but comparatively little literature.',
274+ formula: 'percentile(mortality_count) − percentile(publications_5y)',
275+ formulaVersion: 'ci-research-gap-v1',
276+ unit: 'percentile_points',
277+ higherIsWorse: true,
278+ aggregation: 'none',
279+ validDimensions: ['geography', 'year'],
280+ sourceSlugs: ['pubmed', 'cdc-wonder', 'seer', 'iarc-globocan'],
281+ category: 'unmet_need',
282+ eligibility: { requires: ['mortality_count', 'publications_5y'], entityLevel: 'top' },
283+ experimental: false,
284+ },
285+];
added packages/database/src/seed.ts +49 −0
@@ -0,0 +1,49 @@
1+import { fileURLToPath } from 'node:url';
2+import path from 'node:path';
3+import { eq, sql } from 'drizzle-orm';
4+import { getDb, closeDb } from './client.js';
5+import { geographies, metricDefinitions } from './schema/index.js';
6+import { mintId } from './ids.js';
7+import { METRIC_CATALOG } from './seed-data/metrics.js';
8+import { GEOGRAPHY_SEED } from './seed-data/geographies.js';
9+
10+/**
11+ * Seeds only system data (CLAUDE.md §358): metric definitions and canonical geographies.
12+ * Scientific data is never seeded — it comes from connectors. Sources are synced from connector
13+ * manifests by `pnpm cix sources:sync` (lives in the root CLI to avoid a package cycle).
14+ */
15+export async function seed(): Promise<void> {
16+ const db = getDb({ max: 2 });
17+ for (const m of METRIC_CATALOG) {
18+ const [existing] = await db.select({ id: metricDefinitions.id }).from(metricDefinitions).where(eq(metricDefinitions.slug, m.slug)).limit(1);
19+ const values = { ...m, updatedAt: new Date() };
20+ if (existing) await db.update(metricDefinitions).set(values).where(eq(metricDefinitions.id, existing.id));
21+ else await db.insert(metricDefinitions).values({ id: await mintId(db, 'METRIC'), ...m });
22+ }
23+ const bySlug = new Map<string, string>();
24+ for (const g of GEOGRAPHY_SEED) {
25+ const [existing] = await db.select({ id: geographies.id }).from(geographies).where(eq(geographies.slug, g.slug)).limit(1);
26+ const parentId = g.parentSlug ? bySlug.get(g.parentSlug) ?? null : null;
27+ if (existing) {
28+ await db.update(geographies).set({ name: g.name, kind: g.kind, iso2: g.iso2 ?? null, iso3: g.iso3 ?? null, parentId, whoRegion: g.whoRegion ?? null }).where(eq(geographies.id, existing.id));
29+ bySlug.set(g.slug, existing.id);
30+ } else {
31+ const id = await mintId(db, 'GEO');
32+ await db.insert(geographies).values({ id, slug: g.slug, name: g.name, kind: g.kind, iso2: g.iso2 ?? null, iso3: g.iso3 ?? null, parentId, whoRegion: g.whoRegion ?? null });
33+ bySlug.set(g.slug, id);
34+ }
35+ }
36+ const [{ n }] = (await db.execute<{ n: string }>(sql`SELECT count(*)::text AS n FROM metric_definitions`)) as unknown as [{ n: string }];
37+ console.log(`[seed] metrics=${n} geographies=${GEOGRAPHY_SEED.length}`);
38+}
39+
40+const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
41+if (isMain) {
42+ seed()
43+ .then(() => closeDb())
44+ .catch(async (e) => {
45+ console.error('[seed] failed', e);
46+ await closeDb();
47+ process.exit(1);
48+ });
49+}
added packages/database/tsconfig.json +8 −0
@@ -0,0 +1,8 @@
1+{
2+ "extends": "../../tsconfig.base.json",
3+ "compilerOptions": {
4+ "rootDir": ".",
5+ "noEmit": true
6+ },
7+ "include": ["src", "drizzle.config.ts"]
8+}
added packages/ontology/package.json +27 −0
@@ -0,0 +1,27 @@
1+{
2+ "name": "@cancerindex/ontology",
3+ "version": "0.1.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": {
8+ "types": "./src/index.ts",
9+ "default": "./src/index.ts"
10+ }
11+ },
12+ "scripts": {
13+ "build": "tsc -p tsconfig.json --noEmit",
14+ "typecheck": "tsc -p tsconfig.json --noEmit",
15+ "test": "vitest run --passWithNoTests"
16+ },
17+ "dependencies": {
18+ "@cancerindex/database": "workspace:*",
19+ "@cancerindex/shared": "workspace:*",
20+ "drizzle-orm": "^0.45.0"
21+ },
22+ "devDependencies": {
23+ "@types/node": "^24.0.0",
24+ "typescript": "^5.9.3",
25+ "vitest": "^3.2.0"
26+ }
27+}
added packages/ontology/src/index.ts +3 −0
@@ -0,0 +1,3 @@
1+export * from './qualifiers.js';
2+export * from './reconcile.js';
3+export * from './top-level.js';
added packages/ontology/src/qualifiers.ts +76 −0
@@ -0,0 +1,76 @@
1+/**
2+ * NCIt neoplasm concepts that are *qualified disease states* rather than taxonomy nodes
3+ * (CLAUDE.md §217-218: "Do not create a unique canonical cancer entity for every arbitrary
4+ * combination"). Stage / recurrence / resectability / laterality / treatment-history qualified
5+ * concepts are retained as source records and mapped to their base disease, but are not canonical
6+ * cancers.
7+ *
8+ * Rules are deterministic and tested (test/qualifiers.test.ts). When in doubt a concept is KEPT
9+ * (a false negative costs an extra node; a false positive loses a real disease entity).
10+ */
11+
12+/** Words that mark a clinical state, never a disease identity. */
13+const STATE_WORDS =
14+ /\b(stage\s?[0iv]+[abcs]?\d?|stages?\s[0iv]+|recurrent|refractory|relapsed|resectable|unresectable|borderline resectable|localized|locally advanced|advanced|metastatic|progressive|residual|persistent|newly diagnosed|untreated|previously treated|previously untreated|treatment[- ]naive|bilateral|unilateral|synchronous|metachronous|multifocal|multicentric|unifocal|occult|extensive[- ]stage|limited[- ]stage|in remission|in relapse|in complete remission|minimal residual disease|post[- ]?transplant(ation)?|aggravated|complicated|uncomplicated|with (unknown|known) primary site|of unknown primary origin)\b/i;
15+
16+/** Staging-system suffixes: "… AJCC v8", "… FIGO Stage IIIC". */
17+const STAGING_SUFFIX = /\s(by\s)?(ajcc|figo|uicc|inrg|inss|cog|enneking|lugano|ann arbor|binet|rai|durie[- ]salmon|r?-?iss|tnm)(\sv\d+)?(\sstage\s?[0iv]+[abc]?)?$/i;
18+
19+/** Laterality prefixes only count when followed by an organ-level disease. */
20+const LATERALITY_PREFIX = /^(left|right)\s/i;
21+
22+/**
23+ * Recognized disease names that contain a state word as part of their identity.
24+ * Kept deliberately short: the regexes above are the primary mechanism.
25+ */
26+const KEEP_EXACT = new Set(
27+ [
28+ 'Progressive Multifocal Leukoencephalopathy',
29+ 'Persistent Mullerian Duct Syndrome',
30+ 'Advanced Sclerosing Systemic Mastocytosis',
31+ 'Residual Neuroblastoma',
32+ 'Cancer of Unknown Primary Origin',
33+ 'Carcinoma of Unknown Primary Origin',
34+ 'Neoplasm of Unknown Primary Origin',
35+ 'Adenocarcinoma of Unknown Primary Origin',
36+ 'Squamous Cell Carcinoma of Unknown Primary Origin',
37+ 'Neuroendocrine Carcinoma of Unknown Primary Origin',
38+ 'Melanoma of Unknown Primary Origin',
39+ 'Metastatic Malignant Neoplasm of Unknown Primary Origin',
40+ 'Multifocal Motor Neuropathy',
41+ 'Multicentric Castleman Disease',
42+ 'Idiopathic Multicentric Castleman Disease',
43+ 'HHV8-Associated Multicentric Castleman Disease',
44+ 'Multicentric Reticulohistiocytosis',
45+ 'Multifocal Langerhans Cell Histiocytosis',
46+ 'Unifocal Langerhans Cell Histiocytosis',
47+ 'Multifocal Osteosarcoma',
48+ 'Synchronous Multiple Primary Malignant Neoplasm',
49+ 'Metachronous Multiple Primary Malignant Neoplasm',
50+ 'Occult Breast Carcinoma',
51+ 'Occult Primary Malignant Neoplasm',
52+ ].map((s) => s.toLowerCase()),
53+);
54+
55+/** True when a preferred name denotes a qualified state rather than a disease taxonomy node. */
56+export function isQualifiedState(preferredName: string): boolean {
57+ const name = preferredName.trim().toLowerCase();
58+ if (KEEP_EXACT.has(name)) return false;
59+ if (STAGING_SUFFIX.test(name)) return true;
60+ if (STATE_WORDS.test(name)) return true;
61+ if (LATERALITY_PREFIX.test(name)) return true;
62+ return false;
63+}
64+
65+/** Strip qualifiers to find the base disease label (used to attach qualified states to their disease). */
66+export function baseDiseaseLabel(preferredName: string): string {
67+ let s = preferredName.trim();
68+ for (let i = 0; i < 5; i++) {
69+ const before = s;
70+ s = s.replace(STAGING_SUFFIX, '');
71+ s = s.replace(/^(stage\s?[0iv]+[abcs]?\d?|recurrent|refractory|relapsed|resectable|unresectable|borderline resectable|localized|locally advanced|advanced|metastatic|progressive|residual|persistent|newly diagnosed|untreated|previously treated|previously untreated|treatment[- ]naive|bilateral|unilateral|left|right|synchronous|metachronous|multifocal|multicentric|unifocal|occult|extensive[- ]stage|limited[- ]stage)\s+/i, '');
72+ s = s.replace(/\s+(stage\s?[0iv]+[abcs]?\d?|in remission|in relapse|in complete remission)$/i, '');
73+ if (s === before) break;
74+ }
75+ return s.trim();
76+}
added packages/ontology/src/reconcile.ts +106 −0
@@ -0,0 +1,106 @@
1+import { and, eq, inArray, sql } from 'drizzle-orm';
2+import { normalizeLabel, uninvertMeshTerm, type MatchType } from '@cancerindex/shared';
3+import { type Database, cancerAliases, cancerCodes, cancers } from '@cancerindex/database';
4+
5+export interface CancerMatch {
6+ cancerId: string;
7+ matchType: MatchType;
8+ confidence: number;
9+ via: string; // human-readable explanation
10+}
11+
12+/**
13+ * Cancer label reconciliation (CLAUDE.md §69): identifiers → curated aliases → normalized strings.
14+ * Never uses embeddings or an LLM to merge. Returns null (→ unresolved queue) when unsure.
15+ */
16+export class CancerResolver {
17+ private aliasCache = new Map<string, { cancerId: string; aliasType: string }[]>();
18+ private codeCache = new Map<string, string>();
19+
20+ constructor(private readonly db: Database) {}
21+
22+ /** Preload alias + code lookups in memory for bulk reconciliation. */
23+ async warm(): Promise<void> {
24+ const rows = await this.db
25+ .select({ cancerId: cancerAliases.cancerId, normalized: cancerAliases.normalized, aliasType: cancerAliases.aliasType })
26+ .from(cancerAliases)
27+ .innerJoin(cancers, eq(cancers.id, cancerAliases.cancerId))
28+ .where(eq(cancers.status, 'active'));
29+ this.aliasCache.clear();
30+ for (const r of rows) {
31+ const arr = this.aliasCache.get(r.normalized) ?? [];
32+ arr.push({ cancerId: r.cancerId, aliasType: r.aliasType });
33+ this.aliasCache.set(r.normalized, arr);
34+ }
35+ const codes = await this.db.select({ cancerId: cancerCodes.cancerId, system: cancerCodes.system, code: cancerCodes.code }).from(cancerCodes).where(inArray(cancerCodes.matchType, ['EXACT_IDENTIFIER', 'CURATED_EXACT', 'ONTOLOGY_EXACT']));
36+ this.codeCache.clear();
37+ for (const c of codes) this.codeCache.set(`${c.system}:${c.code}`, c.cancerId);
38+ }
39+
40+ get warmed(): boolean {
41+ return this.aliasCache.size > 0;
42+ }
43+
44+ /** Resolve by external identifier (NCIt code, DOID, OncoTree code, UMLS CUI, MeSH UI…). */
45+ byCode(system: string, code: string): CancerMatch | null {
46+ const id = this.codeCache.get(`${system}:${code}`);
47+ return id ? { cancerId: id, matchType: 'EXACT_IDENTIFIER', confidence: 1, via: `${system}:${code}` } : null;
48+ }
49+
50+ /** Resolve a free-text label. Ambiguous alias hits (≠ 1 distinct cancer) are returned as null. */
51+ byLabel(label: string, opts: { allowMeshInversion?: boolean } = {}): CancerMatch | null {
52+ const candidates = [label];
53+ if (opts.allowMeshInversion && label.includes(',')) candidates.push(uninvertMeshTerm(label));
54+ // Common registry phrasings: "Cancer of the X" → "X cancer"; "Carcinoma of X" → "X carcinoma"
55+ const m = /^(cancer|carcinoma|neoplasm|tumou?r|sarcoma|lymphoma|leukemia|adenocarcinoma|melanoma) of (the )?(.+)$/i.exec(label);
56+ if (m) candidates.push(`${m[3]} ${m[1]}`);
57+ for (const c of candidates) {
58+ const norm = normalizeLabel(c);
59+ if (!norm) continue;
60+ const hits = this.aliasCache.get(norm);
61+ if (!hits || hits.length === 0) continue;
62+ const distinct = [...new Set(hits.map((h) => h.cancerId))];
63+ if (distinct.length === 1) {
64+ const preferred = hits.some((h) => h.aliasType === 'preferred');
65+ return { cancerId: distinct[0]!, matchType: preferred ? 'ONTOLOGY_EXACT' : 'ALIAS', confidence: preferred ? 0.98 : 0.9, via: `alias "${c}"` };
66+ }
67+ // Ambiguous: prefer the cancer where this string is the preferred name.
68+ const pref = hits.filter((h) => h.aliasType === 'preferred');
69+ const prefDistinct = [...new Set(pref.map((h) => h.cancerId))];
70+ if (prefDistinct.length === 1) return { cancerId: prefDistinct[0]!, matchType: 'ONTOLOGY_EXACT', confidence: 0.9, via: `preferred name "${c}" (alias also used by ${distinct.length - 1} other concept(s))` };
71+ return null;
72+ }
73+ return null;
74+ }
75+
76+ /** Try identifiers first, then labels (CLAUDE.md §143, §221). */
77+ resolve(input: { codes?: Array<{ system: string; code: string }>; labels?: string[]; allowMeshInversion?: boolean }): CancerMatch | null {
78+ for (const c of input.codes ?? []) {
79+ const hit = this.byCode(c.system, c.code);
80+ if (hit) return hit;
81+ }
82+ for (const l of input.labels ?? []) {
83+ const hit = this.byLabel(l, { allowMeshInversion: input.allowMeshInversion });
84+ if (hit) return hit;
85+ }
86+ return null;
87+ }
88+
89+ /** Fuzzy suggestion for the curation queue only (never auto-accepted). */
90+ async suggest(label: string): Promise<{ cancerId: string; name: string; score: number } | null> {
91+ const norm = normalizeLabel(label);
92+ if (norm.length < 4) return null;
93+ const rows = await this.db.execute<{ cancer_id: string; canonical_name: string; score: number }>(sql`
94+ SELECT a.cancer_id, c.canonical_name, similarity(a.normalized, ${norm}) AS score
95+ FROM cancer_aliases a JOIN cancers c ON c.id = a.cancer_id
96+ WHERE c.status = 'active' AND a.normalized % ${norm}
97+ ORDER BY score DESC LIMIT 1`);
98+ const r = rows[0];
99+ return r && r.score >= 0.6 ? { cancerId: r.cancer_id, name: r.canonical_name, score: Number(r.score) } : null;
100+ }
101+}
102+
103+export async function loadCancerIdBySlug(db: Database, slug: string): Promise<string | null> {
104+ const [row] = await db.select({ id: cancers.id }).from(cancers).where(and(eq(cancers.slug, slug), eq(cancers.status, 'active'))).limit(1);
105+ return row?.id ?? null;
106+}
added packages/ontology/src/top-level.ts +75 −0
@@ -0,0 +1,75 @@
1+/**
2+ * Explicitly curated metadata (CLAUDE.md §8 allows curated metadata; §246-247 require a
3+ * mutually exclusive top-level set for the global master ranking).
4+ *
5+ * The top-level set follows the GLOBOCAN / ICD-10 site groups so that registry burden estimates
6+ * can be attached without double counting. Each entry maps a site group to its NCIt anchor concept
7+ * (codes verified against the EVS REST API on 2026-09-08, NCIt 26.08e) and to the ICD-10 range
8+ * used by registries. Codes are recorded on `cancer_codes` with match type CURATED_EXACT.
9+ *
10+ * This list is *not* the taxonomy — it is a ranking scope definition (entity_level = "top").
11+ */
12+export interface TopLevelCancer {
13+ key: string;
14+ name: string; // display name (registry phrasing); the canonical NCIt name is kept on the entity
15+ ncit: string;
16+ icd10: string[]; // ICD-10 code ranges, e.g. "C33-C34"
17+ hematologic?: boolean;
18+ globocanCode?: number; // GLOBOCAN cancer code (reference only; ingestion is gated by license review)
19+ notes?: string;
20+}
21+
22+export const TOP_LEVEL_CANCERS: TopLevelCancer[] = [
23+ { key: 'lip-oral-cavity', name: 'Lip and Oral Cavity Cancer', ncit: 'C9316', icd10: ['C00-C06'], globocanCode: 1 },
24+ { key: 'salivary-gland', name: 'Salivary Gland Cancer', ncit: 'C3811', icd10: ['C07-C08'], globocanCode: 2 },
25+ { key: 'oropharynx', name: 'Oropharyngeal Cancer', ncit: 'C7398', icd10: ['C09-C10'], globocanCode: 3 },
26+ { key: 'nasopharynx', name: 'Nasopharyngeal Cancer', ncit: 'C9321', icd10: ['C11'], globocanCode: 4 },
27+ { key: 'hypopharynx', name: 'Hypopharyngeal Cancer', ncit: 'C7190', icd10: ['C12-C13'], globocanCode: 5 },
28+ { key: 'esophagus', name: 'Esophageal Cancer', ncit: 'C7478', icd10: ['C15'], globocanCode: 6 },
29+ { key: 'stomach', name: 'Stomach Cancer', ncit: 'C9331', icd10: ['C16'], globocanCode: 7 },
30+ { key: 'colon', name: 'Colon Cancer', ncit: 'C9242', icd10: ['C18'], globocanCode: 8 },
31+ { key: 'rectum', name: 'Rectal Cancer', ncit: 'C7418', icd10: ['C19-C20'], globocanCode: 9 },
32+ { key: 'anus', name: 'Anal Cancer', ncit: 'C7379', icd10: ['C21'], globocanCode: 10 },
33+ { key: 'liver', name: 'Liver Cancer', ncit: 'C190593', icd10: ['C22'], globocanCode: 11, notes: 'ICD-10 C22 includes intrahepatic bile ducts' },
34+ { key: 'gallbladder', name: 'Gallbladder Cancer', ncit: 'C7481', icd10: ['C23-C24'], globocanCode: 12, notes: 'ICD-10 C23-C24 includes extrahepatic biliary tract; NCIt anchor is gallbladder' },
35+ { key: 'pancreas', name: 'Pancreatic Cancer', ncit: 'C9005', icd10: ['C25'], globocanCode: 13 },
36+ { key: 'larynx', name: 'Laryngeal Cancer', ncit: 'C7484', icd10: ['C32'], globocanCode: 14 },
37+ { key: 'lung', name: 'Lung Cancer', ncit: 'C7377', icd10: ['C33-C34'], globocanCode: 15 },
38+ { key: 'melanoma-skin', name: 'Melanoma of Skin', ncit: 'C3510', icd10: ['C43'], globocanCode: 16 },
39+ { key: 'non-melanoma-skin', name: 'Non-Melanoma Skin Cancer', ncit: 'C4914', icd10: ['C44'], globocanCode: 17, notes: 'registries differ on inclusion (CLAUDE.md §210)' },
40+ { key: 'mesothelioma', name: 'Mesothelioma', ncit: 'C4456', icd10: ['C45'], globocanCode: 18 },
41+ { key: 'kaposi-sarcoma', name: 'Kaposi Sarcoma', ncit: 'C9087', icd10: ['C46'], globocanCode: 19 },
42+ { key: 'breast', name: 'Breast Cancer', ncit: 'C9335', icd10: ['C50'], globocanCode: 20 },
43+ { key: 'vulva', name: 'Vulvar Cancer', ncit: 'C7502', icd10: ['C51'], globocanCode: 21 },
44+ { key: 'vagina', name: 'Vaginal Cancer', ncit: 'C7410', icd10: ['C52'], globocanCode: 22 },
45+ { key: 'cervix', name: 'Cervical Cancer', ncit: 'C9311', icd10: ['C53'], globocanCode: 23 },
46+ { key: 'corpus-uteri', name: 'Uterine Corpus Cancer', ncit: 'C3556', icd10: ['C54'], globocanCode: 24 },
47+ { key: 'ovary', name: 'Ovarian Cancer', ncit: 'C7431', icd10: ['C56'], globocanCode: 25 },
48+ { key: 'penis', name: 'Penile Cancer', ncit: 'C7547', icd10: ['C60'], globocanCode: 26 },
49+ { key: 'prostate', name: 'Prostate Cancer', ncit: 'C7378', icd10: ['C61'], globocanCode: 27 },
50+ { key: 'testis', name: 'Testicular Cancer', ncit: 'C7251', icd10: ['C62'], globocanCode: 28 },
51+ { key: 'kidney', name: 'Kidney Cancer', ncit: 'C7548', icd10: ['C64-C65'], globocanCode: 29 },
52+ { key: 'bladder', name: 'Bladder Cancer', ncit: 'C9334', icd10: ['C67'], globocanCode: 30 },
53+ { key: 'brain-cns', name: 'Brain and Central Nervous System Cancer', ncit: 'C4627', icd10: ['C70-C72'], globocanCode: 31 },
54+ { key: 'thyroid', name: 'Thyroid Cancer', ncit: 'C7510', icd10: ['C73'], globocanCode: 32 },
55+ { key: 'hodgkin-lymphoma', name: 'Hodgkin Lymphoma', ncit: 'C9357', icd10: ['C81'], hematologic: true, globocanCode: 33 },
56+ { key: 'non-hodgkin-lymphoma', name: 'Non-Hodgkin Lymphoma', ncit: 'C3211', icd10: ['C82-C86', 'C96'], hematologic: true, globocanCode: 34 },
57+ { key: 'multiple-myeloma', name: 'Multiple Myeloma', ncit: 'C3242', icd10: ['C88', 'C90'], hematologic: true, globocanCode: 35 },
58+ { key: 'leukemia', name: 'Leukemia', ncit: 'C3161', icd10: ['C91-C95'], hematologic: true, globocanCode: 36 },
59+];
60+
61+export const TOP_LEVEL_BY_NCIT = new Map(TOP_LEVEL_CANCERS.map((t) => [t.ncit, t]));
62+
63+/** NCIt roots used to build the multi-dimensional hierarchy (verified 2026-09-08). */
64+export const NCIT_ROOTS = {
65+ neoplasm: 'C3262',
66+ malignantNeoplasm: 'C9305',
67+ neoplasmBySite: 'C3263',
68+ neoplasmByMorphology: 'C4741',
69+ carcinoma: 'C2916',
70+ malignantSolidNeoplasm: 'C132146',
71+ hematopoieticLymphoidCellNeoplasm: 'C27134',
72+ hematopoieticLymphaticSystemNeoplasm: 'C35813',
73+ childhoodMalignantNeoplasm: 'C4005',
74+ nervousSystemNeoplasm: 'C3268',
75+} as const;
added packages/ontology/test/qualifiers.test.ts +55 −0
@@ -0,0 +1,55 @@
1+import { describe, expect, it } from 'vitest';
2+import { isQualifiedState, baseDiseaseLabel } from '../src/qualifiers.js';
3+
4+describe('isQualifiedState', () => {
5+ it('flags stage / recurrence / resectability concepts', () => {
6+ for (const n of [
7+ 'Stage IV Pancreatic Cancer AJCC v8',
8+ 'Stage IIIA Lung Cancer AJCC v7',
9+ 'Recurrent Glioblastoma',
10+ 'Refractory Acute Myeloid Leukemia',
11+ 'Relapsed Multiple Myeloma',
12+ 'Unresectable Hepatocellular Carcinoma',
13+ 'Locally Advanced Rectal Carcinoma',
14+ 'Metastatic Breast Carcinoma',
15+ 'Newly Diagnosed Glioblastoma',
16+ 'Pancreatic Adenocarcinoma Stage II',
17+ 'Bilateral Breast Carcinoma',
18+ 'Recurrent Childhood Acute Lymphoblastic Leukemia',
19+ ])
20+ expect(isQualifiedState(n), n).toBe(true);
21+ });
22+ it('keeps recognized diseases whose names start with a qualifier-like word', () => {
23+ for (const n of [
24+ 'Anaplastic Astrocytoma',
25+ 'Anaplastic Large Cell Lymphoma',
26+ 'Undifferentiated Pleomorphic Sarcoma',
27+ 'Invasive Breast Carcinoma',
28+ 'Childhood Acute Lymphoblastic Leukemia',
29+ 'Adult T-Cell Leukemia/Lymphoma',
30+ 'Primary Myelofibrosis',
31+ 'Primary Central Nervous System Lymphoma',
32+ 'Therapy-Related Acute Myeloid Leukemia',
33+ 'Triple-Negative Breast Carcinoma',
34+ 'EGFR-Mutant Lung Adenocarcinoma',
35+ 'KRAS G12C-Mutant Non-Small Cell Lung Carcinoma',
36+ 'High Grade Serous Ovarian Carcinoma',
37+ 'Low Grade Glioma',
38+ 'Castration-Resistant Prostate Carcinoma',
39+ 'Pancreatic Ductal Adenocarcinoma',
40+ 'Glioblastoma',
41+ 'Diffuse Large B-Cell Lymphoma',
42+ 'Acute Myeloid Leukemia with NPM1 Mutation',
43+ 'Secondary Acute Myeloid Leukemia',
44+ ])
45+ expect(isQualifiedState(n), n).toBe(false);
46+ });
47+});
48+
49+describe('baseDiseaseLabel', () => {
50+ it('strips stage and recurrence qualifiers', () => {
51+ expect(baseDiseaseLabel('Stage IV Pancreatic Cancer AJCC v8')).toBe('Pancreatic Cancer');
52+ expect(baseDiseaseLabel('Recurrent Metastatic Breast Carcinoma')).toBe('Breast Carcinoma');
53+ expect(baseDiseaseLabel('Glioblastoma')).toBe('Glioblastoma');
54+ });
55+});
added packages/ontology/tsconfig.json +8 −0
@@ -0,0 +1,8 @@
1+{
2+ "extends": "../../tsconfig.base.json",
3+ "compilerOptions": {
4+ "rootDir": ".",
5+ "noEmit": true
6+ },
7+ "include": ["src", "test"]
8+}
added packages/ontology/vitest.config.ts +3 −0
@@ -0,0 +1,3 @@
1+import { defineConfig } from 'vitest/config';
2+
3+export default defineConfig({ test: { include: ['src/**/*.test.ts', 'test/**/*.test.ts'] } });
added packages/ranking/package.json +28 −0
@@ -0,0 +1,28 @@
1+{
2+ "name": "@cancerindex/ranking",
3+ "version": "0.1.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": {
8+ "types": "./src/index.ts",
9+ "default": "./src/index.ts"
10+ }
11+ },
12+ "scripts": {
13+ "build": "tsc -p tsconfig.json --noEmit",
14+ "typecheck": "tsc -p tsconfig.json --noEmit",
15+ "test": "vitest run --passWithNoTests"
16+ },
17+ "dependencies": {
18+ "@cancerindex/database": "workspace:*",
19+ "@cancerindex/ontology": "workspace:*",
20+ "@cancerindex/shared": "workspace:*",
21+ "drizzle-orm": "^0.45.0"
22+ },
23+ "devDependencies": {
24+ "@types/node": "^24.0.0",
25+ "typescript": "^5.9.3",
26+ "vitest": "^3.2.0"
27+ }
28+}
added packages/ranking/src/counters.ts +140 −0
@@ -0,0 +1,140 @@
1+import { sql } from 'drizzle-orm';
2+import type { Database } from '@cancerindex/database';
3+
4+const ACTIVE_STATUSES = ['RECRUITING', 'NOT_YET_RECRUITING', 'ENROLLING_BY_INVITATION', 'ACTIVE_NOT_RECRUITING'];
5+
6+/**
7+ * Recompute entity_counters for every active cancer from canonical relations (CLAUDE.md §287).
8+ * Trial / evidence / cohort counts aggregate over the NCIt-hierarchy descendants of each cancer
9+ * (a lung-cancer trial mapped to "Lung Adenocarcinoma" counts for "Lung Cancer" too).
10+ * Deterministic: a full rebuild in one SQL transaction.
11+ */
12+export async function refreshCounters(db: Database): Promise<number> {
13+ await db.execute(sql`
14+ CREATE TEMP TABLE IF NOT EXISTS _desc (ancestor varchar(32), descendant varchar(32)) ON COMMIT DROP;
15+ `);
16+ return db.transaction(async (tx) => {
17+ await tx.execute(sql`CREATE TEMP TABLE _desc (ancestor varchar(32), descendant varchar(32)) ON COMMIT DROP`);
18+ await tx.execute(sql`
19+ INSERT INTO _desc
20+ WITH RECURSIVE d AS (
21+ SELECT id AS ancestor, id AS descendant, 0 AS depth FROM cancers WHERE status = 'active'
22+ UNION
23+ SELECT d.ancestor, h.child_id, d.depth + 1 FROM d JOIN cancer_hierarchy h ON h.parent_id = d.descendant
24+ WHERE d.depth < 12
25+ )
26+ SELECT DISTINCT ancestor, descendant FROM d
27+ `);
28+ await tx.execute(sql`CREATE INDEX ON _desc (descendant)`);
29+ await tx.execute(sql`CREATE INDEX ON _desc (ancestor)`);
30+ const rows = await tx.execute<{ n: string }>(sql`
31+ WITH trial_map AS (
32+ SELECT DISTINCT d.ancestor AS cancer_id, t.id AS trial_id, t.overall_status, t.phases, t.study_type
33+ FROM trial_conditions tc JOIN _desc d ON d.descendant = tc.cancer_id JOIN clinical_trials t ON t.id = tc.trial_id
34+ WHERE tc.cancer_id IS NOT NULL
35+ ),
36+ trials AS (
37+ SELECT cancer_id,
38+ count(*) AS trial_count,
39+ count(*) FILTER (WHERE study_type = 'INTERVENTIONAL' AND overall_status = ANY(${ACTIVE_STATUSES}::text[])) AS active_trial_count,
40+ count(*) FILTER (WHERE study_type = 'INTERVENTIONAL' AND overall_status = 'RECRUITING') AS recruiting_trial_count,
41+ count(*) FILTER (WHERE study_type = 'INTERVENTIONAL' AND overall_status = ANY(${ACTIVE_STATUSES}::text[]) AND 'PHASE3' = ANY(phases)) AS phase3_trial_count
42+ FROM trial_map GROUP BY cancer_id
43+ ),
44+ pubs AS (
45+ SELECT cancer_id,
46+ max(count) FILTER (WHERE window_key = 'all') AS publication_count,
47+ max(count) FILTER (WHERE window_key = '5y') AS publication_count_5y,
48+ max(count) FILTER (WHERE window_key = '12m') AS publication_count_12m
49+ FROM literature_counts GROUP BY cancer_id
50+ ),
51+ evid AS (
52+ SELECT d.ancestor AS cancer_id, count(DISTINCT e.civic_id) AS evidence_count,
53+ count(DISTINCT g) FILTER (WHERE g IS NOT NULL) AS civic_gene_count,
54+ count(DISTINCT v) FILTER (WHERE v IS NOT NULL) AS variant_count,
55+ count(DISTINCT t) FILTER (WHERE t IS NOT NULL) AS drug_count
56+ FROM civic_evidence_items e JOIN _desc d ON d.descendant = e.cancer_id
57+ LEFT JOIN LATERAL unnest(e.gene_ids) g ON true
58+ LEFT JOIN LATERAL unnest(e.variant_ids) v ON true
59+ LEFT JOIN LATERAL unnest(e.therapy_ids) t ON true
60+ WHERE e.status = 'ACCEPTED' GROUP BY d.ancestor
61+ ),
62+ cohorts AS (
63+ SELECT d.ancestor AS cancer_id, count(DISTINCT c.id) AS cohort_count,
64+ count(DISTINCT f.gene_symbol) FILTER (WHERE f.frequency >= 0.05 AND f.cases_affected >= 20) AS gdc_gene_count
65+ FROM genomic_cohorts c JOIN _desc d ON d.descendant = c.cancer_id
66+ LEFT JOIN cancer_gene_frequencies f ON f.cohort_id = c.id
67+ GROUP BY d.ancestor
68+ ),
69+ appr AS (
70+ SELECT d.ancestor AS cancer_id, count(DISTINCT a.drug_id) AS approved_drug_count
71+ FROM drug_approvals a JOIN _desc d ON d.descendant = a.cancer_id WHERE a.status IN ('approved','accelerated','conditional') GROUP BY d.ancestor
72+ ),
73+ epi AS (
74+ SELECT cancer_id, count(*) AS n FROM epidemiology_observations GROUP BY cancer_id
75+ ),
76+ surv AS (
77+ SELECT cancer_id, count(*) AS n FROM survival_observations GROUP BY cancer_id
78+ ),
79+ tree AS (
80+ SELECT ancestor AS cancer_id, count(*) - 1 AS descendant_count FROM _desc GROUP BY ancestor
81+ ),
82+ kids AS (
83+ SELECT parent_id AS cancer_id, count(DISTINCT child_id) AS subtype_count FROM cancer_hierarchy GROUP BY parent_id
84+ ),
85+ ins AS (
86+ INSERT INTO entity_counters (entity_type, entity_id, trial_count, active_trial_count, recruiting_trial_count, phase3_trial_count,
87+ publication_count, publication_count_5y, publication_count_12m, gene_count, variant_count, drug_count, approved_drug_count,
88+ evidence_count, cohort_count, subtype_count, descendant_count, epidemiology_obs_count, survival_obs_count, completeness, computed_at)
89+ SELECT 'cancer', c.id,
90+ COALESCE(t.trial_count,0), COALESCE(t.active_trial_count,0), COALESCE(t.recruiting_trial_count,0), COALESCE(t.phase3_trial_count,0),
91+ COALESCE(p.publication_count,0), COALESCE(p.publication_count_5y,0), COALESCE(p.publication_count_12m,0),
92+ GREATEST(COALESCE(e.civic_gene_count,0), COALESCE(g.gdc_gene_count,0)), COALESCE(e.variant_count,0), COALESCE(e.drug_count,0), COALESCE(a.approved_drug_count,0),
93+ COALESCE(e.evidence_count,0), COALESCE(g.cohort_count,0), COALESCE(k.subtype_count,0), COALESCE(tr.descendant_count,0), COALESCE(ep.n,0), COALESCE(sv.n,0),
94+ jsonb_build_object(
95+ 'epidemiology', CASE WHEN COALESCE(ep.n,0) > 0 THEN 1 ELSE 0 END,
96+ 'survival', CASE WHEN COALESCE(sv.n,0) > 0 THEN 1 ELSE 0 END,
97+ 'trials', CASE WHEN COALESCE(t.trial_count,0) > 0 THEN 1 ELSE 0 END,
98+ 'literature', CASE WHEN COALESCE(p.publication_count,0) > 0 THEN 1 ELSE 0 END,
99+ 'genomics', CASE WHEN COALESCE(g.cohort_count,0) > 0 THEN 1 ELSE 0 END,
100+ 'evidence', CASE WHEN COALESCE(e.evidence_count,0) > 0 THEN 1 ELSE 0 END,
101+ 'therapies', CASE WHEN COALESCE(a.approved_drug_count,0) > 0 THEN 1 ELSE 0 END
102+ ), now()
103+ FROM cancers c
104+ LEFT JOIN trials t ON t.cancer_id = c.id
105+ LEFT JOIN pubs p ON p.cancer_id = c.id
106+ LEFT JOIN evid e ON e.cancer_id = c.id
107+ LEFT JOIN cohorts g ON g.cancer_id = c.id
108+ LEFT JOIN appr a ON a.cancer_id = c.id
109+ LEFT JOIN epi ep ON ep.cancer_id = c.id
110+ LEFT JOIN surv sv ON sv.cancer_id = c.id
111+ LEFT JOIN tree tr ON tr.cancer_id = c.id
112+ LEFT JOIN kids k ON k.cancer_id = c.id
113+ WHERE c.status = 'active'
114+ ON CONFLICT (entity_type, entity_id) DO UPDATE SET
115+ trial_count = EXCLUDED.trial_count, active_trial_count = EXCLUDED.active_trial_count, recruiting_trial_count = EXCLUDED.recruiting_trial_count,
116+ phase3_trial_count = EXCLUDED.phase3_trial_count, publication_count = EXCLUDED.publication_count, publication_count_5y = EXCLUDED.publication_count_5y,
117+ publication_count_12m = EXCLUDED.publication_count_12m, gene_count = EXCLUDED.gene_count, variant_count = EXCLUDED.variant_count, drug_count = EXCLUDED.drug_count,
118+ approved_drug_count = EXCLUDED.approved_drug_count, evidence_count = EXCLUDED.evidence_count, cohort_count = EXCLUDED.cohort_count, subtype_count = EXCLUDED.subtype_count,
119+ descendant_count = EXCLUDED.descendant_count, epidemiology_obs_count = EXCLUDED.epidemiology_obs_count, survival_obs_count = EXCLUDED.survival_obs_count,
120+ completeness = EXCLUDED.completeness, computed_at = now()
121+ RETURNING 1
122+ )
123+ SELECT count(*)::text AS n FROM ins
124+ `);
125+ // Gene-level counters (cancers linked, evidence items) for gene pages.
126+ await tx.execute(sql`
127+ INSERT INTO entity_counters (entity_type, entity_id, evidence_count, variant_count, drug_count, trial_count, computed_at)
128+ SELECT 'gene', g.id,
129+ (SELECT count(*) FROM civic_evidence_items e WHERE e.status = 'ACCEPTED' AND g.id = ANY(e.gene_ids)),
130+ (SELECT count(*) FROM variants v WHERE v.gene_id = g.id),
131+ (SELECT count(DISTINCT t) FROM civic_evidence_items e, unnest(e.therapy_ids) t WHERE e.status = 'ACCEPTED' AND g.id = ANY(e.gene_ids)),
132+ 0, now()
133+ FROM genes g
134+ WHERE EXISTS (SELECT 1 FROM civic_evidence_items e WHERE g.id = ANY(e.gene_ids)) OR EXISTS (SELECT 1 FROM variants v WHERE v.gene_id = g.id) OR EXISTS (SELECT 1 FROM cancer_gene_frequencies f WHERE f.gene_id = g.id)
135+ ON CONFLICT (entity_type, entity_id) DO UPDATE SET evidence_count = EXCLUDED.evidence_count, variant_count = EXCLUDED.variant_count, drug_count = EXCLUDED.drug_count, computed_at = now()
136+ `);
137+ await tx.execute(sql`UPDATE genes g SET is_cancer_gene = EXISTS (SELECT 1 FROM civic_evidence_items e WHERE e.status = 'ACCEPTED' AND g.id = ANY(e.gene_ids)) OR EXISTS (SELECT 1 FROM cancer_gene_frequencies f WHERE f.gene_id = g.id AND f.frequency >= 0.05 AND f.cases_affected >= 20)`);
138+ return Number(rows[0]?.n ?? 0);
139+ });
140+}
added packages/ranking/src/engine.ts +173 −0
@@ -0,0 +1,173 @@
1+import { and, eq, sql } from 'drizzle-orm';
2+import { type Database, metricDefinitions, rankingSnapshots, rankings, entityCounters, cancers } from '@cancerindex/database';
3+import { rankEntities, inputsHash, percentiles, type RankInput } from './rank.js';
4+
5+export interface Scope {
6+ geography: string; // WORLD | ISO3 | slug
7+ sex: 'all' | 'male' | 'female';
8+ ageGroup: string;
9+ year: number | null;
10+ entityLevel: 'top' | 'all';
11+}
12+
13+export function scopeKey(s: Scope): string {
14+ return `geo=${s.geography}|sex=${s.sex}|age=${s.ageGroup}|year=${s.year ?? 'latest'}|level=${s.entityLevel}`;
15+}
16+
17+export interface RankingResult {
18+ metricSlug: string;
19+ scopeKey: string;
20+ eligible: number;
21+ snapshotId: number;
22+}
23+
24+/**
25+ * Ranking engine (CLAUDE.md §30-35, §245-247, §288). Every snapshot stores scope, formula version,
26+ * an inputs hash and per-row lineage (`inputs`) so "Why #4?" and TRACE can be answered.
27+ *
28+ * Phase 1 ships count-based metrics (trials, literature, molecular) for both entity levels and the
29+ * burden/lethality/gap metrics only where epidemiology observations exist for the scope.
30+ */
31+export async function computeAllRankings(db: Database): Promise<RankingResult[]> {
32+ const results: RankingResult[] = [];
33+ const defs = await db.select().from(metricDefinitions);
34+ const byslug = new Map(defs.map((d) => [d.slug, d]));
35+
36+ const countMetrics: Array<[string, keyof typeof entityCounters.$inferSelect]> = [
37+ ['active_trials', 'activeTrialCount'],
38+ ['recruiting_trials', 'recruitingTrialCount'],
39+ ['phase3_trials', 'phase3TrialCount'],
40+ ['publications_5y', 'publicationCount5y'],
41+ ['publications_12m', 'publicationCount12m'],
42+ ['curated_evidence_items', 'evidenceCount'],
43+ ['associated_genes', 'geneCount'],
44+ ['genomic_cohorts', 'cohortCount'],
45+ ];
46+
47+ for (const level of ['top', 'all'] as const) {
48+ const scope: Scope = { geography: 'WORLD', sex: 'all', ageGroup: 'all', year: null, entityLevel: level };
49+ const counters = await db
50+ .select({ id: cancers.id, c: entityCounters })
51+ .from(cancers)
52+ .innerJoin(entityCounters, and(eq(entityCounters.entityType, 'cancer'), eq(entityCounters.entityId, cancers.id)))
53+ .where(level === 'top' ? and(eq(cancers.status, 'active'), eq(cancers.topLevel, true)) : and(eq(cancers.status, 'active'), eq(cancers.malignant, true)));
54+ for (const [slug, field] of countMetrics) {
55+ const def = byslug.get(slug);
56+ if (!def) continue;
57+ const items: RankInput[] = counters
58+ .map((r) => ({ id: r.id, value: Number(r.c[field] ?? 0), inputs: { counter: field, computedAt: r.c.computedAt } }))
59+ .filter((i) => (slug.startsWith('publications') ? i.value > 0 : true)); // publications: only cancers with a stored PubMed query
60+ if (items.length === 0) continue;
61+ const snap = await persistSnapshot(db, def, scope, items, { descending: true, sourceIds: def.sourceSlugs });
62+ results.push(snap);
63+ }
64+ // Publication growth (derived from two windows).
65+ const growthDef = byslug.get('publication_growth');
66+ if (growthDef) {
67+ const rows = await db.execute<{ cancer_id: string; m12: number; y5: number }>(sql`
68+ SELECT cancer_id, max(count) FILTER (WHERE window_key = '12m') AS m12, max(count) FILTER (WHERE window_key = '5y_prior') AS y5
69+ FROM literature_counts GROUP BY cancer_id`);
70+ const ids = new Set(counters.map((c) => c.id));
71+ const items: RankInput[] = rows
72+ .filter((r) => ids.has(r.cancer_id) && r.y5 != null && r.m12 != null && Number(r.y5) >= 50)
73+ .map((r) => ({ id: r.cancer_id, value: Number(r.m12) / (Number(r.y5) / 5), inputs: { publications_12m: Number(r.m12), publications_5y_prior: Number(r.y5), formula: growthDef.formula } }));
74+ if (items.length) results.push(await persistSnapshot(db, growthDef, scope, items, { descending: true, sourceIds: ['pubmed'] }));
75+ }
76+ }
77+
78+ // Burden / lethality / gaps per geography-year where observations exist (top level only, §246-247).
79+ const scopes = await db.execute<{ geography_id: string; slug: string; iso3: string | null; year: number; sex: string; source_id: string; n: number }>(sql`
80+ SELECT o.geography_id, g.slug, g.iso3, o.year, o.sex, o.source_id, count(*) AS n
81+ FROM epidemiology_observations o JOIN geographies g ON g.id = o.geography_id JOIN cancers c ON c.id = o.cancer_id
82+ WHERE c.top_level AND o.age_group = 'all' AND o.metric IN ('incidence_count','mortality_count','as_incidence_rate','as_mortality_rate')
83+ GROUP BY o.geography_id, g.slug, g.iso3, o.year, o.sex, o.source_id HAVING count(*) >= 10`);
84+ for (const s of scopes) {
85+ const scope: Scope = { geography: s.iso3 ?? s.slug.toUpperCase(), sex: s.sex as Scope['sex'], ageGroup: 'all', year: Number(s.year), entityLevel: 'top' };
86+ const obs = await db.execute<{ cancer_id: string; metric: string; value: number; id: number; estimate_type: string; site_definition: string | null }>(sql`
87+ SELECT o.cancer_id, o.metric, o.value, o.id, o.estimate_type, o.site_definition FROM epidemiology_observations o JOIN cancers c ON c.id = o.cancer_id
88+ WHERE c.top_level AND o.geography_id = ${s.geography_id} AND o.year = ${s.year} AND o.sex = ${s.sex} AND o.age_group = 'all' AND o.source_id = ${s.source_id}`);
89+ const byMetric = new Map<string, Map<string, { value: number; id: number; estimate_type: string; site: string | null }>>();
90+ for (const o of obs) {
91+ if (!byMetric.has(o.metric)) byMetric.set(o.metric, new Map());
92+ byMetric.get(o.metric)!.set(o.cancer_id, { value: Number(o.value), id: o.id, estimate_type: o.estimate_type, site: o.site_definition });
93+ }
94+ for (const metric of ['incidence_count', 'mortality_count', 'as_incidence_rate', 'as_mortality_rate']) {
95+ const def = byslug.get(metric);
96+ const m = byMetric.get(metric);
97+ if (!def || !m) continue;
98+ const items: RankInput[] = [...m.entries()].map(([id, v]) => ({ id, value: v.value, confidence: v.estimate_type === 'observed' ? 'HIGH' : 'MEDIUM', inputs: { observationId: v.id, estimateType: v.estimate_type, siteDefinition: v.site, sourceId: s.source_id } }));
99+ results.push(await persistSnapshot(db, def, scope, items, { descending: true, sourceIds: [s.source_id] }));
100+ }
101+ // Mortality-to-incidence ratio
102+ const mirDef = byslug.get('mortality_incidence_ratio');
103+ const inc = byMetric.get('incidence_count');
104+ const mort = byMetric.get('mortality_count');
105+ if (mirDef && inc && mort) {
106+ const items: RankInput[] = [];
107+ for (const [id, i] of inc) {
108+ const d = mort.get(id);
109+ if (!d || i.value < 100) continue;
110+ items.push({ id, value: d.value / i.value, confidence: i.value >= 1000 ? 'HIGH' : 'MEDIUM', inputs: { incidenceObservationId: i.id, mortalityObservationId: d.id, incidence: i.value, deaths: d.value, formula: mirDef.formula } });
111+ }
112+ if (items.length) results.push(await persistSnapshot(db, mirDef, scope, items, { descending: true, sourceIds: [s.source_id] }));
113+ }
114+ // Gap indexes: burden percentile − activity percentile
115+ if (mort) {
116+ const burdenPct = percentiles([...mort.entries()].map(([id, v]) => ({ id, value: v.value })));
117+ const counters = await db.select({ id: entityCounters.entityId, trials: entityCounters.activeTrialCount, pubs: entityCounters.publicationCount5y }).from(entityCounters).where(eq(entityCounters.entityType, 'cancer'));
118+ const cmap = new Map(counters.map((c) => [c.id, c]));
119+ for (const [slug, field] of [
120+ ['trial_gap', 'trials'],
121+ ['research_gap', 'pubs'],
122+ ] as const) {
123+ const def = byslug.get(slug);
124+ if (!def) continue;
125+ const ids = [...mort.keys()].filter((id) => cmap.has(id) && (field === 'trials' || (cmap.get(id)!.pubs ?? 0) > 0));
126+ if (ids.length < 5) continue;
127+ const actPct = percentiles(ids.map((id) => ({ id, value: Number(cmap.get(id)![field] ?? 0) })));
128+ const items: RankInput[] = ids.map((id) => ({ id, value: (burdenPct.get(id) ?? 0) - (actPct.get(id) ?? 0), inputs: { burdenPercentile: burdenPct.get(id), activityPercentile: actPct.get(id), deaths: mort.get(id)!.value, activity: Number(cmap.get(id)![field] ?? 0), formula: def.formula } }));
129+ results.push(await persistSnapshot(db, def, scope, items, { descending: true, sourceIds: [s.source_id, ...def.sourceSlugs] }));
130+ }
131+ }
132+ }
133+ return results;
134+}
135+
136+async function persistSnapshot(db: Database, def: typeof metricDefinitions.$inferSelect, scope: Scope, items: RankInput[], opts: { descending: boolean; sourceIds: string[] }): Promise<RankingResult> {
137+ const key = scopeKey(scope);
138+ const ranked = rankEntities(items, { descending: def.higherIsWorse === false ? opts.descending : opts.descending });
139+ const hash = await inputsHash(items);
140+ return db.transaction(async (tx) => {
141+ // Previous rank for change explanation (§311)
142+ const prev = await tx.execute<{ cancer_id: string; rank: number }>(sql`
143+ SELECT r.cancer_id, r.rank FROM rankings r JOIN ranking_snapshots s ON s.id = r.snapshot_id
144+ WHERE s.metric_slug = ${def.slug} AND s.scope_key = ${key} AND s.is_current`);
145+ const prevMap = new Map(prev.map((p) => [p.cancer_id, Number(p.rank)]));
146+ await tx.update(rankingSnapshots).set({ isCurrent: false }).where(and(eq(rankingSnapshots.metricSlug, def.slug), eq(rankingSnapshots.scopeKey, key), eq(rankingSnapshots.isCurrent, true)));
147+ const [snap] = await tx
148+ .insert(rankingSnapshots)
149+ .values({ metricId: def.id, metricSlug: def.slug, scopeKey: key, geography: scope.geography, sex: scope.sex, ageGroup: scope.ageGroup, year: scope.year, entityLevel: scope.entityLevel, formulaVersion: def.formulaVersion, eligibleEntities: ranked.length, inputsHash: hash, sourceIds: opts.sourceIds, isCurrent: true })
150+ .returning({ id: rankingSnapshots.id });
151+ const snapshotId = snap!.id;
152+ const chunk = 500;
153+ for (let i = 0; i < ranked.length; i += chunk) {
154+ await tx.insert(rankings).values(
155+ ranked.slice(i, i + chunk).map((r) => ({
156+ snapshotId,
157+ metricSlug: def.slug,
158+ scopeKey: key,
159+ cancerId: r.id,
160+ rank: r.rank,
161+ eligibleEntities: r.eligible,
162+ percentile: r.percentile,
163+ value: r.value,
164+ unit: def.unit,
165+ confidence: r.confidence ?? 'MEDIUM',
166+ inputs: r.inputs ?? {},
167+ previousRank: prevMap.get(r.id) ?? null,
168+ })),
169+ );
170+ }
171+ return { metricSlug: def.slug, scopeKey: key, eligible: ranked.length, snapshotId };
172+ });
173+}
added packages/ranking/src/index.ts +4 −0
@@ -0,0 +1,4 @@
1+export * from './rank.js';
2+export * from './counters.js';
3+export * from './engine.js';
4+export * from './trace.js';
added packages/ranking/src/rank.ts +50 −0
@@ -0,0 +1,50 @@
1+/**
2+ * Pure ranking primitives (CLAUDE.md §155: deterministic given fixed inputs).
3+ */
4+export interface RankInput {
5+ id: string;
6+ value: number;
7+ /** Optional confidence label carried through to the output. */
8+ confidence?: 'HIGH' | 'MEDIUM' | 'LOW' | 'INSUFFICIENT_DATA';
9+ inputs?: Record<string, unknown>;
10+}
11+
12+export interface RankOutput extends RankInput {
13+ rank: number;
14+ percentile: number; // 0..100, 100 = most extreme in the ranking direction
15+ eligible: number;
16+}
17+
18+/**
19+ * Rank entities. `higherIsWorse === null` means "higher is first" (descending) by convention.
20+ * Ties share the same rank (competition ranking: 1,2,2,4). Percentile = share of eligible entities
21+ * ranked at or below this entity in the ranking direction.
22+ */
23+export function rankEntities(items: RankInput[], opts: { descending?: boolean } = {}): RankOutput[] {
24+ const desc = opts.descending ?? true;
25+ const valid = items.filter((i) => Number.isFinite(i.value));
26+ const sorted = [...valid].sort((a, b) => (desc ? b.value - a.value : a.value - b.value) || a.id.localeCompare(b.id));
27+ const n = sorted.length;
28+ const out: RankOutput[] = [];
29+ let rank = 0;
30+ for (let i = 0; i < n; i++) {
31+ const cur = sorted[i]!;
32+ if (i === 0 || cur.value !== sorted[i - 1]!.value) rank = i + 1;
33+ out.push({ ...cur, rank, eligible: n, percentile: n === 1 ? 100 : Math.round(((n - rank) / (n - 1)) * 1000) / 10 });
34+ }
35+ return out;
36+}
37+
38+/** Percentile of each entity for a metric (0..100, higher value → higher percentile). */
39+export function percentiles(items: RankInput[]): Map<string, number> {
40+ const ranked = rankEntities(items, { descending: true });
41+ return new Map(ranked.map((r) => [r.id, r.percentile]));
42+}
43+
44+/** Stable hash of ranking inputs so a snapshot is reproducible/auditable. */
45+export async function inputsHash(items: RankInput[]): Promise<string> {
46+ const { createHash } = await import('node:crypto');
47+ const h = createHash('sha256');
48+ for (const i of [...items].sort((a, b) => a.id.localeCompare(b.id))) h.update(`${i.id}=${i.value};`);
49+ return h.digest('hex').slice(0, 24);
50+}
added packages/ranking/src/trace.ts +51 −0
@@ -0,0 +1,51 @@
1+import { sql } from 'drizzle-orm';
2+import type { Database } from '@cancerindex/database';
3+
4+/**
5+ * Lineage trace (CLAUDE.md §252-253): ranked value → inputs → observation → provenance → raw record.
6+ * Supported: rankings/<id>, epidemiology_observations/<id>, cancer_gene_frequencies/<id>, literature_counts/<id>.
7+ */
8+export async function traceValue(db: Database, table: string, id: string): Promise<Record<string, unknown>> {
9+ const out: Record<string, unknown> = { table, id };
10+ const prov = async (pid: number | null | undefined) => {
11+ if (!pid) return null;
12+ const [p] = await db.execute<Record<string, unknown>>(sql`SELECT p.*, s.name AS source_name, s.slug AS source_slug, s.license FROM provenance p JOIN sources s ON s.id = p.source_id WHERE p.id = ${pid}`);
13+ if (!p) return null;
14+ const [rec] = await db.execute<Record<string, unknown>>(sql`SELECT id, entity_kind, source_record_id, raw_path, payload_hash, retrieved_at, status FROM source_records WHERE source_id = ${p.source_id as string} AND source_record_id = ${(p.source_record_id as string) ?? ''} LIMIT 1`);
15+ return { provenance: p, sourceRecord: rec ?? null };
16+ };
17+ switch (table) {
18+ case 'rankings': {
19+ const [r] = await db.execute<Record<string, unknown>>(sql`SELECT r.*, s.formula_version, s.inputs_hash, s.generated_at AS snapshot_generated_at, m.formula, m.name AS metric_name FROM rankings r JOIN ranking_snapshots s ON s.id = r.snapshot_id JOIN metric_definitions m ON m.slug = r.metric_slug WHERE r.id = ${Number(id)}`);
20+ if (!r) return { ...out, error: 'not found' };
21+ out.ranking = r;
22+ const inputs = (r.inputs ?? {}) as Record<string, unknown>;
23+ for (const k of ['observationId', 'incidenceObservationId', 'mortalityObservationId']) {
24+ if (inputs[k]) out[k] = await traceValue(db, 'epidemiology_observations', String(inputs[k]));
25+ }
26+ return out;
27+ }
28+ case 'epidemiology_observations': {
29+ const [o] = await db.execute<Record<string, unknown>>(sql`SELECT * FROM epidemiology_observations WHERE id = ${Number(id)}`);
30+ if (!o) return { ...out, error: 'not found' };
31+ return { ...out, observation: o, lineage: await prov(o.provenance_id as number) };
32+ }
33+ case 'cancer_gene_frequencies': {
34+ const [o] = await db.execute<Record<string, unknown>>(sql`SELECT * FROM cancer_gene_frequencies WHERE id = ${Number(id)}`);
35+ if (!o) return { ...out, error: 'not found' };
36+ return { ...out, frequency: o, lineage: await prov(o.provenance_id as number) };
37+ }
38+ case 'literature_counts': {
39+ const [o] = await db.execute<Record<string, unknown>>(sql`SELECT * FROM literature_counts WHERE id = ${Number(id)}`);
40+ if (!o) return { ...out, error: 'not found' };
41+ return { ...out, literatureCount: o, lineage: await prov(o.provenance_id as number) };
42+ }
43+ case 'survival_observations': {
44+ const [o] = await db.execute<Record<string, unknown>>(sql`SELECT * FROM survival_observations WHERE id = ${Number(id)}`);
45+ if (!o) return { ...out, error: 'not found' };
46+ return { ...out, observation: o, lineage: await prov(o.provenance_id as number) };
47+ }
48+ default:
49+ return { ...out, error: 'unsupported table' };
50+ }
51+}
added packages/ranking/test/rank.test.ts +33 −0
@@ -0,0 +1,33 @@
1+import { describe, expect, it } from 'vitest';
2+import { rankEntities, percentiles, inputsHash } from '../src/rank.js';
3+
4+describe('rankEntities', () => {
5+ it('is deterministic and handles ties with competition ranking', () => {
6+ const items = [
7+ { id: 'b', value: 10 },
8+ { id: 'a', value: 10 },
9+ { id: 'c', value: 5 },
10+ { id: 'd', value: 1 },
11+ ];
12+ const r = rankEntities(items);
13+ expect(r.map((x) => [x.id, x.rank])).toEqual([
14+ ['a', 1],
15+ ['b', 1],
16+ ['c', 3],
17+ ['d', 4],
18+ ]);
19+ expect(r[0]!.percentile).toBe(100);
20+ expect(r[3]!.percentile).toBe(0);
21+ expect(rankEntities([...items].reverse())).toEqual(r);
22+ });
23+ it('drops non-finite values and ranks ascending when requested', () => {
24+ const r = rankEntities([{ id: 'x', value: NaN }, { id: 'y', value: 2 }, { id: 'z', value: 1 }], { descending: false });
25+ expect(r.map((x) => x.id)).toEqual(['z', 'y']);
26+ expect(r[0]!.eligible).toBe(2);
27+ });
28+ it('percentiles and hash are stable', async () => {
29+ const items = [{ id: 'a', value: 3 }, { id: 'b', value: 1 }];
30+ expect(percentiles(items).get('a')).toBe(100);
31+ expect(await inputsHash(items)).toBe(await inputsHash([...items].reverse()));
32+ });
33+});
added packages/ranking/tsconfig.json +8 −0
@@ -0,0 +1,8 @@
1+{
2+ "extends": "../../tsconfig.base.json",
3+ "compilerOptions": {
4+ "rootDir": ".",
5+ "noEmit": true
6+ },
7+ "include": ["src", "test"]
8+}
added packages/ranking/vitest.config.ts +3 −0
@@ -0,0 +1,3 @@
1+import { defineConfig } from 'vitest/config';
2+
3+export default defineConfig({ test: { include: ['src/**/*.test.ts', 'test/**/*.test.ts'] } });
added packages/shared/package.json +26 −0
@@ -0,0 +1,26 @@
1+{
2+ "name": "@cancerindex/shared",
3+ "version": "0.1.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": {
8+ "types": "./src/index.ts",
9+ "default": "./src/index.ts"
10+ }
11+ },
12+ "scripts": {
13+ "build": "tsc -p tsconfig.json --noEmit",
14+ "typecheck": "tsc -p tsconfig.json --noEmit",
15+ "test": "vitest run --passWithNoTests"
16+ },
17+ "dependencies": {
18+ "pino": "^9.7.0",
19+ "zod": "^4.0.0"
20+ },
21+ "devDependencies": {
22+ "@types/node": "^24.0.0",
23+ "typescript": "^5.9.3",
24+ "vitest": "^3.2.0"
25+ }
26+}
added packages/shared/src/env.ts +16 −0
@@ -0,0 +1,16 @@
1+import path from 'node:path';
2+
3+export function dataDir(): string {
4+ return path.resolve(process.env.CI_DATA_DIR ?? path.join(process.cwd(), 'data'));
5+}
6+
7+export function isProduction(): boolean {
8+ return process.env.NODE_ENV === 'production';
9+}
10+
11+export function envInt(name: string, fallback: number): number {
12+ const v = process.env[name];
13+ if (!v) return fallback;
14+ const n = Number(v);
15+ return Number.isFinite(n) ? n : fallback;
16+}
added packages/shared/src/ids.ts +70 −0
@@ -0,0 +1,70 @@
1+/**
2+ * CancerIndex stable public identifiers (CLAUDE.md §6).
3+ *
4+ * CI-CAN-00000001 cancer entity
5+ * CI-GENE-00000001 gene
6+ * CI-VAR-00000001 variant
7+ * CI-DRUG-00000001 drug
8+ * CI-TRIAL-00000001 clinical trial
9+ * CI-PUB-00000001 publication
10+ * CI-BIO-00000001 biomarker
11+ * CI-STUDY-00000001 genomic study / cohort
12+ * CI-METRIC-00000001 metric definition
13+ * CI-SOURCE-00000001 source
14+ * CI-ORG-00000001 organization
15+ * CI-TRT-00000001 treatment / regimen
16+ * CI-ANAT-00000001 anatomical site
17+ * CI-GEO-00000001 geography
18+ * CI-PROV-00000001 provenance record
19+ *
20+ * IDs are minted from a database sequence per namespace (see @cancerindex/database `mintId`) and
21+ * are never reused. Database auto-increment integers are never exposed publicly.
22+ */
23+export const ID_NAMESPACES = [
24+ 'CAN',
25+ 'GENE',
26+ 'VAR',
27+ 'DRUG',
28+ 'TRIAL',
29+ 'PUB',
30+ 'BIO',
31+ 'STUDY',
32+ 'METRIC',
33+ 'SOURCE',
34+ 'ORG',
35+ 'TRT',
36+ 'ANAT',
37+ 'GEO',
38+ 'PROV',
39+ 'EDGE',
40+ 'RANK',
41+] as const;
42+export type IdNamespace = (typeof ID_NAMESPACES)[number];
43+
44+export const ID_WIDTH = 8;
45+
46+export function formatId(ns: IdNamespace, n: number): string {
47+ if (!Number.isInteger(n) || n < 1) throw new Error(`invalid id number ${n}`);
48+ return `CI-${ns}-${String(n).padStart(ID_WIDTH, '0')}`;
49+}
50+
51+const ID_RE = /^CI-([A-Z]+)-(\d{8,})$/;
52+
53+export function parseId(id: string): { ns: IdNamespace; n: number } | null {
54+ const m = ID_RE.exec(id);
55+ if (!m) return null;
56+ const ns = m[1] as IdNamespace;
57+ if (!ID_NAMESPACES.includes(ns)) return null;
58+ return { ns, n: Number(m[2]) };
59+}
60+
61+export function isCiId(id: string, ns?: IdNamespace): boolean {
62+ const p = parseId(id);
63+ return !!p && (ns ? p.ns === ns : true);
64+}
65+
66+/** Ingest run identifiers: ING-CLINICALTRIALS-20260908-000019 (CLAUDE.md §176). */
67+export function formatRunId(connectorId: string, date: Date, seq: number): string {
68+ const d = date.toISOString().slice(0, 10).replace(/-/g, '');
69+ return `ING-${connectorId.toUpperCase().replace(/[^A-Z0-9]/g, '')}-${d}-${String(seq).padStart(6, '0')}`;
70+}
added packages/shared/src/index.ts +5 −0
@@ -0,0 +1,5 @@
1+export * from './ids.js';
2+export * from './provenance.js';
3+export * from './normalize.js';
4+export * from './logger.js';
5+export * from './env.js';
added packages/shared/src/logger.ts +9 −0
@@ -0,0 +1,9 @@
1+import pino from 'pino';
2+
3+export const logger = pino({
4+ level: process.env.LOG_LEVEL ?? 'info',
5+ base: { service: process.env.CI_SERVICE ?? 'cancerindex' },
6+ timestamp: pino.stdTimeFunctions.isoTime,
7+});
8+
9+export type Logger = typeof logger;
added packages/shared/src/normalize.ts +40 −0
@@ -0,0 +1,40 @@
1+/**
2+ * String normalization used by reconciliation (CLAUDE.md §69). Deterministic, no LLM.
3+ */
4+
5+const GREEK: Record<string, string> = { α: 'alpha', β: 'beta', γ: 'gamma', δ: 'delta', κ: 'kappa', λ: 'lambda', μ: 'mu' };
6+
7+export function stripDiacritics(s: string): string {
8+ return s.normalize('NFKD').replace(/[̀-ͯ]/g, '');
9+}
10+
11+/** Lowercase, ASCII, punctuation → space, collapse whitespace. Keeps digits (G12C, HER2). */
12+export function normalizeLabel(input: string): string {
13+ let s = stripDiacritics(input).toLowerCase();
14+ s = s.replace(/[αβγδκλμ]/g, (c) => GREEK[c] ?? c);
15+ s = s.replace(/['’`"]/g, '');
16+ s = s.replace(/[^a-z0-9]+/g, ' ');
17+ s = s.replace(/\b(nos|unspecified|not otherwise specified)\b/g, ' ');
18+ s = s.replace(/\bcarcinomas\b/g, 'carcinoma').replace(/\bcancers\b/g, 'cancer').replace(/\btumou?rs\b/g, 'tumor').replace(/\btumour\b/g, 'tumor').replace(/\bneoplasms\b/g, 'neoplasm').replace(/\bsarcomas\b/g, 'sarcoma').replace(/\blymphomas\b/g, 'lymphoma').replace(/\bleukaemia\b/g, 'leukemia').replace(/\bleukemias\b/g, 'leukemia');
19+ return s.replace(/\s+/g, ' ').trim();
20+}
21+
22+/** "Carcinoma, Non-Small-Cell Lung" (MeSH inverted form) → "Non-Small-Cell Lung Carcinoma". */
23+export function uninvertMeshTerm(term: string): string {
24+ const parts = term.split(',').map((p) => p.trim());
25+ if (parts.length < 2) return term;
26+ return [...parts.slice(1), parts[0]].join(' ');
27+}
28+
29+/** URL slug for entity routes. */
30+export function slugify(input: string): string {
31+ return stripDiacritics(input)
32+ .toLowerCase()
33+ .replace(/[^a-z0-9]+/g, '-')
34+ .replace(/^-+|-+$/g, '')
35+ .slice(0, 120);
36+}
37+
38+export function sha256Hex(data: string | Uint8Array): Promise<string> {
39+ return import('node:crypto').then(({ createHash }) => createHash('sha256').update(data).digest('hex'));
40+}
added packages/shared/src/provenance.ts +85 −0
@@ -0,0 +1,85 @@
1+import { z } from 'zod';
2+
3+/** CLAUDE.md §2 — every imported fact carries provenance; every derived value is reproducible. */
4+export const EvidenceType = z.enum([
5+ 'registry',
6+ 'clinical_trial',
7+ 'meta_analysis',
8+ 'systematic_review',
9+ 'cohort',
10+ 'case_control',
11+ 'case_series',
12+ 'case_report',
13+ 'preclinical',
14+ 'regulatory',
15+ 'guideline',
16+ 'expert_curation',
17+ 'database',
18+ 'computed',
19+]);
20+export type EvidenceType = z.infer<typeof EvidenceType>;
21+
22+export const AccessLevel = z.enum(['open', 'registration_required', 'controlled', 'licensed']);
23+export type AccessLevel = z.infer<typeof AccessLevel>;
24+
25+export const ProvenanceInput = z.object({
26+ sourceId: z.string(),
27+ sourceRecordId: z.string().optional(),
28+ sourceUrl: z.string().url().optional(),
29+ dataset: z.string().optional(),
30+ datasetVersion: z.string().optional(),
31+ publicationId: z.string().optional(),
32+ pmid: z.string().optional(),
33+ doi: z.string().optional(),
34+ retrievedAt: z.string().datetime(),
35+ publishedAt: z.string().optional(),
36+ updatedAt: z.string().optional(),
37+ geography: z.string().optional(),
38+ population: z.string().optional(),
39+ cohortSize: z.number().int().nonnegative().optional(),
40+ methodology: z.string().optional(),
41+ evidenceType: EvidenceType,
42+ accessLevel: AccessLevel,
43+ confidence: z.number().min(0).max(1).optional(),
44+ license: z.string().optional(),
45+ ingestRunId: z.string().optional(),
46+});
47+export type ProvenanceInput = z.infer<typeof ProvenanceInput>;
48+
49+/** Data layer a value belongs to (CLAUDE.md §2 — layers stay separable). */
50+export const DataLayer = z.enum(['raw', 'normalized', 'canonical', 'derived', 'ranked', 'ai_synthesis']);
51+export type DataLayer = z.infer<typeof DataLayer>;
52+
53+/** Display category for scientific safety (CLAUDE.md §3). */
54+export const ClaimCategory = z.enum([
55+ 'observed_data',
56+ 'published_evidence',
57+ 'curated_evidence',
58+ 'regulatory_status',
59+ 'clinical_guideline',
60+ 'computed_metric',
61+ 'ai_generated_synthesis',
62+]);
63+export type ClaimCategory = z.infer<typeof ClaimCategory>;
64+
65+/** Entity mapping confidence (CLAUDE.md §221). */
66+export const MatchType = z.enum([
67+ 'EXACT_IDENTIFIER',
68+ 'CURATED_EXACT',
69+ 'ONTOLOGY_EXACT',
70+ 'CURATED_BROADER',
71+ 'CURATED_NARROWER',
72+ 'ALIAS',
73+ 'PROBABILISTIC',
74+ 'UNRESOLVED',
75+]);
76+export type MatchType = z.infer<typeof MatchType>;
77+
78+/** Derived statistic envelope (CLAUDE.md §2). */
79+export interface DerivedValue {
80+ metric: string;
81+ value: number;
82+ computed: true;
83+ formulaVersion: string;
84+ inputs: string[];
85+}
added packages/shared/tsconfig.json +9 −0
@@ -0,0 +1,9 @@
1+{
2+ "extends": "../../tsconfig.base.json",
3+ "compilerOptions": {
4+ "rootDir": "src",
5+ "outDir": "dist",
6+ "noEmit": true
7+ },
8+ "include": ["src"]
9+}
added pnpm-lock.yaml +2023 −0
@@ -0,0 +1,2023 @@
1+lockfileVersion: '9.0'
2+
3+settings:
4+ autoInstallPeers: true
5+ excludeLinksFromLockfile: false
6+
7+importers:
8+
9+ .:
10+ dependencies:
11+ '@cancerindex/connectors':
12+ specifier: workspace:*
13+ version: link:packages/connectors
14+ '@cancerindex/database':
15+ specifier: workspace:*
16+ version: link:packages/database
17+ '@cancerindex/ontology':
18+ specifier: workspace:*
19+ version: link:packages/ontology
20+ '@cancerindex/ranking':
21+ specifier: workspace:*
22+ version: link:packages/ranking
23+ '@cancerindex/shared':
24+ specifier: workspace:*
25+ version: link:packages/shared
26+ drizzle-orm:
27+ specifier: ^0.45.0
28+ version: 0.45.2(postgres@3.4.9)
29+ postgres:
30+ specifier: ^3.4.7
31+ version: 3.4.9
32+ devDependencies:
33+ '@types/node':
34+ specifier: ^24.0.0
35+ version: 24.13.3
36+ tsx:
37+ specifier: ^4.20.0
38+ version: 4.23.13
39+ typescript:
40+ specifier: ^5.9.3
41+ version: 5.9.3
42+ vitest:
43+ specifier: ^3.2.0
44+ version: 3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
45+
46+ packages/connectors:
47+ dependencies:
48+ '@cancerindex/database':
49+ specifier: workspace:*
50+ version: link:../database
51+ '@cancerindex/ontology':
52+ specifier: workspace:*
53+ version: link:../ontology
54+ '@cancerindex/shared':
55+ specifier: workspace:*
56+ version: link:../shared
57+ drizzle-orm:
58+ specifier: ^0.45.0
59+ version: 0.45.2(postgres@3.4.9)
60+ fast-xml-parser:
61+ specifier: ^5.2.0
62+ version: 5.11.1
63+ postgres:
64+ specifier: ^3.4.7
65+ version: 3.4.9
66+ yaml:
67+ specifier: ^2.8.0
68+ version: 2.9.0
69+ zod:
70+ specifier: ^4.0.0
71+ version: 4.5.4
72+ devDependencies:
73+ '@types/node':
74+ specifier: ^24.0.0
75+ version: 24.13.3
76+ typescript:
77+ specifier: ^5.9.3
78+ version: 5.9.3
79+ vitest:
80+ specifier: ^3.2.0
81+ version: 3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
82+
83+ packages/database:
84+ dependencies:
85+ '@cancerindex/shared':
86+ specifier: workspace:*
87+ version: link:../shared
88+ drizzle-orm:
89+ specifier: ^0.45.0
90+ version: 0.45.2(postgres@3.4.9)
91+ postgres:
92+ specifier: ^3.4.7
93+ version: 3.4.9
94+ devDependencies:
95+ '@types/node':
96+ specifier: ^24.0.0
97+ version: 24.13.3
98+ drizzle-kit:
99+ specifier: ^0.31.0
100+ version: 0.31.10
101+ tsx:
102+ specifier: ^4.20.0
103+ version: 4.23.13
104+ typescript:
105+ specifier: ^5.9.3
106+ version: 5.9.3
107+ vitest:
108+ specifier: ^3.2.0
109+ version: 3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
110+
111+ packages/ontology:
112+ dependencies:
113+ '@cancerindex/database':
114+ specifier: workspace:*
115+ version: link:../database
116+ '@cancerindex/shared':
117+ specifier: workspace:*
118+ version: link:../shared
119+ drizzle-orm:
120+ specifier: ^0.45.0
121+ version: 0.45.2(postgres@3.4.9)
122+ devDependencies:
123+ '@types/node':
124+ specifier: ^24.0.0
125+ version: 24.13.3
126+ typescript:
127+ specifier: ^5.9.3
128+ version: 5.9.3
129+ vitest:
130+ specifier: ^3.2.0
131+ version: 3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
132+
133+ packages/ranking:
134+ dependencies:
135+ '@cancerindex/database':
136+ specifier: workspace:*
137+ version: link:../database
138+ '@cancerindex/ontology':
139+ specifier: workspace:*
140+ version: link:../ontology
141+ '@cancerindex/shared':
142+ specifier: workspace:*
143+ version: link:../shared
144+ drizzle-orm:
145+ specifier: ^0.45.0
146+ version: 0.45.2(postgres@3.4.9)
147+ devDependencies:
148+ '@types/node':
149+ specifier: ^24.0.0
150+ version: 24.13.3
151+ typescript:
152+ specifier: ^5.9.3
153+ version: 5.9.3
154+ vitest:
155+ specifier: ^3.2.0
156+ version: 3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
157+
158+ packages/shared:
159+ dependencies:
160+ pino:
161+ specifier: ^9.7.0
162+ version: 9.14.0
163+ zod:
164+ specifier: ^4.0.0
165+ version: 4.5.4
166+ devDependencies:
167+ '@types/node':
168+ specifier: ^24.0.0
169+ version: 24.13.3
170+ typescript:
171+ specifier: ^5.9.3
172+ version: 5.9.3
173+ vitest:
174+ specifier: ^3.2.0
175+ version: 3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
176+
177+packages:
178+
179+ '@drizzle-team/brocli@0.10.2':
180+ resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
181+
182+ '@esbuild-kit/core-utils@3.3.2':
183+ resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
184+ deprecated: 'Merged into tsx: https://tsx.hirok.io'
185+
186+ '@esbuild-kit/esm-loader@2.6.5':
187+ resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
188+ deprecated: 'Merged into tsx: https://tsx.hirok.io'
189+
190+ '@esbuild/aix-ppc64@0.25.12':
191+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
192+ engines: {node: '>=18'}
193+ cpu: [ppc64]
194+ os: [aix]
195+
196+ '@esbuild/aix-ppc64@0.28.2':
197+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
198+ engines: {node: '>=18'}
199+ cpu: [ppc64]
200+ os: [aix]
201+
202+ '@esbuild/android-arm64@0.18.20':
203+ resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
204+ engines: {node: '>=12'}
205+ cpu: [arm64]
206+ os: [android]
207+
208+ '@esbuild/android-arm64@0.25.12':
209+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
210+ engines: {node: '>=18'}
211+ cpu: [arm64]
212+ os: [android]
213+
214+ '@esbuild/android-arm64@0.28.2':
215+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
216+ engines: {node: '>=18'}
217+ cpu: [arm64]
218+ os: [android]
219+
220+ '@esbuild/android-arm@0.18.20':
221+ resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
222+ engines: {node: '>=12'}
223+ cpu: [arm]
224+ os: [android]
225+
226+ '@esbuild/android-arm@0.25.12':
227+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
228+ engines: {node: '>=18'}
229+ cpu: [arm]
230+ os: [android]
231+
232+ '@esbuild/android-arm@0.28.2':
233+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
234+ engines: {node: '>=18'}
235+ cpu: [arm]
236+ os: [android]
237+
238+ '@esbuild/android-x64@0.18.20':
239+ resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
240+ engines: {node: '>=12'}
241+ cpu: [x64]
242+ os: [android]
243+
244+ '@esbuild/android-x64@0.25.12':
245+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
246+ engines: {node: '>=18'}
247+ cpu: [x64]
248+ os: [android]
249+
250+ '@esbuild/android-x64@0.28.2':
251+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
252+ engines: {node: '>=18'}
253+ cpu: [x64]
254+ os: [android]
255+
256+ '@esbuild/darwin-arm64@0.18.20':
257+ resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
258+ engines: {node: '>=12'}
259+ cpu: [arm64]
260+ os: [darwin]
261+
262+ '@esbuild/darwin-arm64@0.25.12':
263+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
264+ engines: {node: '>=18'}
265+ cpu: [arm64]
266+ os: [darwin]
267+
268+ '@esbuild/darwin-arm64@0.28.2':
269+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
270+ engines: {node: '>=18'}
271+ cpu: [arm64]
272+ os: [darwin]
273+
274+ '@esbuild/darwin-x64@0.18.20':
275+ resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
276+ engines: {node: '>=12'}
277+ cpu: [x64]
278+ os: [darwin]
279+
280+ '@esbuild/darwin-x64@0.25.12':
281+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
282+ engines: {node: '>=18'}
283+ cpu: [x64]
284+ os: [darwin]
285+
286+ '@esbuild/darwin-x64@0.28.2':
287+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
288+ engines: {node: '>=18'}
289+ cpu: [x64]
290+ os: [darwin]
291+
292+ '@esbuild/freebsd-arm64@0.18.20':
293+ resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
294+ engines: {node: '>=12'}
295+ cpu: [arm64]
296+ os: [freebsd]
297+
298+ '@esbuild/freebsd-arm64@0.25.12':
299+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
300+ engines: {node: '>=18'}
301+ cpu: [arm64]
302+ os: [freebsd]
303+
304+ '@esbuild/freebsd-arm64@0.28.2':
305+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
306+ engines: {node: '>=18'}
307+ cpu: [arm64]
308+ os: [freebsd]
309+
310+ '@esbuild/freebsd-x64@0.18.20':
311+ resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
312+ engines: {node: '>=12'}
313+ cpu: [x64]
314+ os: [freebsd]
315+
316+ '@esbuild/freebsd-x64@0.25.12':
317+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
318+ engines: {node: '>=18'}
319+ cpu: [x64]
320+ os: [freebsd]
321+
322+ '@esbuild/freebsd-x64@0.28.2':
323+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
324+ engines: {node: '>=18'}
325+ cpu: [x64]
326+ os: [freebsd]
327+
328+ '@esbuild/linux-arm64@0.18.20':
329+ resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
330+ engines: {node: '>=12'}
331+ cpu: [arm64]
332+ os: [linux]
333+
334+ '@esbuild/linux-arm64@0.25.12':
335+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
336+ engines: {node: '>=18'}
337+ cpu: [arm64]
338+ os: [linux]
339+
340+ '@esbuild/linux-arm64@0.28.2':
341+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
342+ engines: {node: '>=18'}
343+ cpu: [arm64]
344+ os: [linux]
345+
346+ '@esbuild/linux-arm@0.18.20':
347+ resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
348+ engines: {node: '>=12'}
349+ cpu: [arm]
350+ os: [linux]
351+
352+ '@esbuild/linux-arm@0.25.12':
353+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
354+ engines: {node: '>=18'}
355+ cpu: [arm]
356+ os: [linux]
357+
358+ '@esbuild/linux-arm@0.28.2':
359+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
360+ engines: {node: '>=18'}
361+ cpu: [arm]
362+ os: [linux]
363+
364+ '@esbuild/linux-ia32@0.18.20':
365+ resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
366+ engines: {node: '>=12'}
367+ cpu: [ia32]
368+ os: [linux]
369+
370+ '@esbuild/linux-ia32@0.25.12':
371+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
372+ engines: {node: '>=18'}
373+ cpu: [ia32]
374+ os: [linux]
375+
376+ '@esbuild/linux-ia32@0.28.2':
377+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
378+ engines: {node: '>=18'}
379+ cpu: [ia32]
380+ os: [linux]
381+
382+ '@esbuild/linux-loong64@0.18.20':
383+ resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
384+ engines: {node: '>=12'}
385+ cpu: [loong64]
386+ os: [linux]
387+
388+ '@esbuild/linux-loong64@0.25.12':
389+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
390+ engines: {node: '>=18'}
391+ cpu: [loong64]
392+ os: [linux]
393+
394+ '@esbuild/linux-loong64@0.28.2':
395+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
396+ engines: {node: '>=18'}
397+ cpu: [loong64]
398+ os: [linux]
399+
400+ '@esbuild/linux-mips64el@0.18.20':
401+ resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
402+ engines: {node: '>=12'}
403+ cpu: [mips64el]
404+ os: [linux]
405+
406+ '@esbuild/linux-mips64el@0.25.12':
407+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
408+ engines: {node: '>=18'}
409+ cpu: [mips64el]
410+ os: [linux]
411+
412+ '@esbuild/linux-mips64el@0.28.2':
413+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
414+ engines: {node: '>=18'}
415+ cpu: [mips64el]
416+ os: [linux]
417+
418+ '@esbuild/linux-ppc64@0.18.20':
419+ resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
420+ engines: {node: '>=12'}
421+ cpu: [ppc64]
422+ os: [linux]
423+
424+ '@esbuild/linux-ppc64@0.25.12':
425+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
426+ engines: {node: '>=18'}
427+ cpu: [ppc64]
428+ os: [linux]
429+
430+ '@esbuild/linux-ppc64@0.28.2':
431+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
432+ engines: {node: '>=18'}
433+ cpu: [ppc64]
434+ os: [linux]
435+
436+ '@esbuild/linux-riscv64@0.18.20':
437+ resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
438+ engines: {node: '>=12'}
439+ cpu: [riscv64]
440+ os: [linux]
441+
442+ '@esbuild/linux-riscv64@0.25.12':
443+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
444+ engines: {node: '>=18'}
445+ cpu: [riscv64]
446+ os: [linux]
447+
448+ '@esbuild/linux-riscv64@0.28.2':
449+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
450+ engines: {node: '>=18'}
451+ cpu: [riscv64]
452+ os: [linux]
453+
454+ '@esbuild/linux-s390x@0.18.20':
455+ resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
456+ engines: {node: '>=12'}
457+ cpu: [s390x]
458+ os: [linux]
459+
460+ '@esbuild/linux-s390x@0.25.12':
461+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
462+ engines: {node: '>=18'}
463+ cpu: [s390x]
464+ os: [linux]
465+
466+ '@esbuild/linux-s390x@0.28.2':
467+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
468+ engines: {node: '>=18'}
469+ cpu: [s390x]
470+ os: [linux]
471+
472+ '@esbuild/linux-x64@0.18.20':
473+ resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
474+ engines: {node: '>=12'}
475+ cpu: [x64]
476+ os: [linux]
477+
478+ '@esbuild/linux-x64@0.25.12':
479+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
480+ engines: {node: '>=18'}
481+ cpu: [x64]
482+ os: [linux]
483+
484+ '@esbuild/linux-x64@0.28.2':
485+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
486+ engines: {node: '>=18'}
487+ cpu: [x64]
488+ os: [linux]
489+
490+ '@esbuild/netbsd-arm64@0.25.12':
491+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
492+ engines: {node: '>=18'}
493+ cpu: [arm64]
494+ os: [netbsd]
495+
496+ '@esbuild/netbsd-arm64@0.28.2':
497+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
498+ engines: {node: '>=18'}
499+ cpu: [arm64]
500+ os: [netbsd]
501+
502+ '@esbuild/netbsd-x64@0.18.20':
503+ resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
504+ engines: {node: '>=12'}
505+ cpu: [x64]
506+ os: [netbsd]
507+
508+ '@esbuild/netbsd-x64@0.25.12':
509+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
510+ engines: {node: '>=18'}
511+ cpu: [x64]
512+ os: [netbsd]
513+
514+ '@esbuild/netbsd-x64@0.28.2':
515+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
516+ engines: {node: '>=18'}
517+ cpu: [x64]
518+ os: [netbsd]
519+
520+ '@esbuild/openbsd-arm64@0.25.12':
521+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
522+ engines: {node: '>=18'}
523+ cpu: [arm64]
524+ os: [openbsd]
525+
526+ '@esbuild/openbsd-arm64@0.28.2':
527+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
528+ engines: {node: '>=18'}
529+ cpu: [arm64]
530+ os: [openbsd]
531+
532+ '@esbuild/openbsd-x64@0.18.20':
533+ resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
534+ engines: {node: '>=12'}
535+ cpu: [x64]
536+ os: [openbsd]
537+
538+ '@esbuild/openbsd-x64@0.25.12':
539+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
540+ engines: {node: '>=18'}
541+ cpu: [x64]
542+ os: [openbsd]
543+
544+ '@esbuild/openbsd-x64@0.28.2':
545+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
546+ engines: {node: '>=18'}
547+ cpu: [x64]
548+ os: [openbsd]
549+
550+ '@esbuild/openharmony-arm64@0.25.12':
551+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
552+ engines: {node: '>=18'}
553+ cpu: [arm64]
554+ os: [openharmony]
555+
556+ '@esbuild/openharmony-arm64@0.28.2':
557+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
558+ engines: {node: '>=18'}
559+ cpu: [arm64]
560+ os: [openharmony]
561+
562+ '@esbuild/sunos-x64@0.18.20':
563+ resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
564+ engines: {node: '>=12'}
565+ cpu: [x64]
566+ os: [sunos]
567+
568+ '@esbuild/sunos-x64@0.25.12':
569+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
570+ engines: {node: '>=18'}
571+ cpu: [x64]
572+ os: [sunos]
573+
574+ '@esbuild/sunos-x64@0.28.2':
575+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
576+ engines: {node: '>=18'}
577+ cpu: [x64]
578+ os: [sunos]
579+
580+ '@esbuild/win32-arm64@0.18.20':
581+ resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
582+ engines: {node: '>=12'}
583+ cpu: [arm64]
584+ os: [win32]
585+
586+ '@esbuild/win32-arm64@0.25.12':
587+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
588+ engines: {node: '>=18'}
589+ cpu: [arm64]
590+ os: [win32]
591+
592+ '@esbuild/win32-arm64@0.28.2':
593+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
594+ engines: {node: '>=18'}
595+ cpu: [arm64]
596+ os: [win32]
597+
598+ '@esbuild/win32-ia32@0.18.20':
599+ resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
600+ engines: {node: '>=12'}
601+ cpu: [ia32]
602+ os: [win32]
603+
604+ '@esbuild/win32-ia32@0.25.12':
605+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
606+ engines: {node: '>=18'}
607+ cpu: [ia32]
608+ os: [win32]
609+
610+ '@esbuild/win32-ia32@0.28.2':
611+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
612+ engines: {node: '>=18'}
613+ cpu: [ia32]
614+ os: [win32]
615+
616+ '@esbuild/win32-x64@0.18.20':
617+ resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
618+ engines: {node: '>=12'}
619+ cpu: [x64]
620+ os: [win32]
621+
622+ '@esbuild/win32-x64@0.25.12':
623+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
624+ engines: {node: '>=18'}
625+ cpu: [x64]
626+ os: [win32]
627+
628+ '@esbuild/win32-x64@0.28.2':
629+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
630+ engines: {node: '>=18'}
631+ cpu: [x64]
632+ os: [win32]
633+
634+ '@jridgewell/sourcemap-codec@1.6.0':
635+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
636+
637+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
638+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
639+ engines: {node: ^22.20 || ^24.12 || >=25}
640+ cpu: [x64]
641+ os: [linux]
642+ libc: [glibc]
643+
644+ '@nodable/entities@3.0.0':
645+ resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==}
646+
647+ '@pinojs/redact@0.4.0':
648+ resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
649+
650+ '@rollup/rollup-android-arm-eabi@4.63.1':
651+ resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==}
652+ cpu: [arm]
653+ os: [android]
654+
655+ '@rollup/rollup-android-arm64@4.63.1':
656+ resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==}
657+ cpu: [arm64]
658+ os: [android]
659+
660+ '@rollup/rollup-darwin-arm64@4.63.1':
661+ resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==}
662+ cpu: [arm64]
663+ os: [darwin]
664+
665+ '@rollup/rollup-darwin-x64@4.63.1':
666+ resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==}
667+ cpu: [x64]
668+ os: [darwin]
669+
670+ '@rollup/rollup-freebsd-arm64@4.63.1':
671+ resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==}
672+ cpu: [arm64]
673+ os: [freebsd]
674+
675+ '@rollup/rollup-freebsd-x64@4.63.1':
676+ resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==}
677+ cpu: [x64]
678+ os: [freebsd]
679+
680+ '@rollup/rollup-linux-arm-gnueabihf@4.63.1':
681+ resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==}
682+ cpu: [arm]
683+ os: [linux]
684+ libc: [glibc]
685+
686+ '@rollup/rollup-linux-arm-musleabihf@4.63.1':
687+ resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==}
688+ cpu: [arm]
689+ os: [linux]
690+ libc: [musl]
691+
692+ '@rollup/rollup-linux-arm64-gnu@4.63.1':
693+ resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==}
694+ cpu: [arm64]
695+ os: [linux]
696+ libc: [glibc]
697+
698+ '@rollup/rollup-linux-arm64-musl@4.63.1':
699+ resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==}
700+ cpu: [arm64]
701+ os: [linux]
702+ libc: [musl]
703+
704+ '@rollup/rollup-linux-loong64-gnu@4.63.1':
705+ resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==}
706+ cpu: [loong64]
707+ os: [linux]
708+ libc: [glibc]
709+
710+ '@rollup/rollup-linux-loong64-musl@4.63.1':
711+ resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==}
712+ cpu: [loong64]
713+ os: [linux]
714+ libc: [musl]
715+
716+ '@rollup/rollup-linux-ppc64-gnu@4.63.1':
717+ resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==}
718+ cpu: [ppc64]
719+ os: [linux]
720+ libc: [glibc]
721+
722+ '@rollup/rollup-linux-ppc64-musl@4.63.1':
723+ resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==}
724+ cpu: [ppc64]
725+ os: [linux]
726+ libc: [musl]
727+
728+ '@rollup/rollup-linux-riscv64-gnu@4.63.1':
729+ resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==}
730+ cpu: [riscv64]
731+ os: [linux]
732+ libc: [glibc]
733+
734+ '@rollup/rollup-linux-riscv64-musl@4.63.1':
735+ resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==}
736+ cpu: [riscv64]
737+ os: [linux]
738+ libc: [musl]
739+
740+ '@rollup/rollup-linux-s390x-gnu@4.63.1':
741+ resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==}
742+ cpu: [s390x]
743+ os: [linux]
744+ libc: [glibc]
745+
746+ '@rollup/rollup-linux-x64-gnu@4.63.1':
747+ resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==}
748+ cpu: [x64]
749+ os: [linux]
750+ libc: [glibc]
751+
752+ '@rollup/rollup-linux-x64-musl@4.63.1':
753+ resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==}
754+ cpu: [x64]
755+ os: [linux]
756+ libc: [musl]
757+
758+ '@rollup/rollup-openbsd-x64@4.63.1':
759+ resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==}
760+ cpu: [x64]
761+ os: [openbsd]
762+
763+ '@rollup/rollup-openharmony-arm64@4.63.1':
764+ resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==}
765+ cpu: [arm64]
766+ os: [openharmony]
767+
768+ '@rollup/rollup-win32-arm64-msvc@4.63.1':
769+ resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==}
770+ cpu: [arm64]
771+ os: [win32]
772+
773+ '@rollup/rollup-win32-ia32-msvc@4.63.1':
774+ resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==}
775+ cpu: [ia32]
776+ os: [win32]
777+
778+ '@rollup/rollup-win32-x64-gnu@4.63.1':
779+ resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==}
780+ cpu: [x64]
781+ os: [win32]
782+
783+ '@rollup/rollup-win32-x64-msvc@4.63.1':
784+ resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==}
785+ cpu: [x64]
786+ os: [win32]
787+
788+ '@types/chai@5.2.3':
789+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
790+
791+ '@types/deep-eql@4.0.2':
792+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
793+
794+ '@types/estree@1.0.9':
795+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
796+
797+ '@types/node@24.13.3':
798+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
799+
800+ '@vitest/expect@3.2.7':
801+ resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==}
802+
803+ '@vitest/mocker@3.2.7':
804+ resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==}
805+ peerDependencies:
806+ msw: ^2.4.9
807+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
808+ peerDependenciesMeta:
809+ msw:
810+ optional: true
811+ vite:
812+ optional: true
813+
814+ '@vitest/pretty-format@3.2.7':
815+ resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==}
816+
817+ '@vitest/runner@3.2.7':
818+ resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==}
819+
820+ '@vitest/snapshot@3.2.7':
821+ resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==}
822+
823+ '@vitest/spy@3.2.7':
824+ resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==}
825+
826+ '@vitest/utils@3.2.7':
827+ resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==}
828+
829+ anynum@1.0.1:
830+ resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==}
831+
832+ assertion-error@2.0.1:
833+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
834+ engines: {node: '>=12'}
835+
836+ atomic-sleep@1.0.0:
837+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
838+ engines: {node: '>=8.0.0'}
839+
840+ buffer-from@1.1.2:
841+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
842+
843+ cac@6.7.14:
844+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
845+ engines: {node: '>=8'}
846+
847+ chai@5.3.3:
848+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
849+ engines: {node: '>=18'}
850+
851+ check-error@2.1.3:
852+ resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
853+ engines: {node: '>= 16'}
854+
855+ debug@4.4.3:
856+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
857+ engines: {node: '>=6.0'}
858+ peerDependencies:
859+ supports-color: '*'
860+ peerDependenciesMeta:
861+ supports-color:
862+ optional: true
863+
864+ deep-eql@5.0.2:
865+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
866+ engines: {node: '>=6'}
867+
868+ drizzle-kit@0.31.10:
869+ resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
870+ hasBin: true
871+
872+ drizzle-orm@0.45.2:
873+ resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==}
874+ peerDependencies:
875+ '@aws-sdk/client-rds-data': '>=3'
876+ '@cloudflare/workers-types': '>=4'
877+ '@electric-sql/pglite': '>=0.2.0'
878+ '@libsql/client': '>=0.10.0'
879+ '@libsql/client-wasm': '>=0.10.0'
880+ '@neondatabase/serverless': '>=0.10.0'
881+ '@op-engineering/op-sqlite': '>=2'
882+ '@opentelemetry/api': ^1.4.1
883+ '@planetscale/database': '>=1.13'
884+ '@prisma/client': '*'
885+ '@tidbcloud/serverless': '*'
886+ '@types/better-sqlite3': '*'
887+ '@types/pg': '*'
888+ '@types/sql.js': '*'
889+ '@upstash/redis': '>=1.34.7'
890+ '@vercel/postgres': '>=0.8.0'
891+ '@xata.io/client': '*'
892+ better-sqlite3: '>=7'
893+ bun-types: '*'
894+ expo-sqlite: '>=14.0.0'
895+ gel: '>=2'
896+ knex: '*'
897+ kysely: '*'
898+ mysql2: '>=2'
899+ pg: '>=8'
900+ postgres: '>=3'
901+ prisma: '*'
902+ sql.js: '>=1'
903+ sqlite3: '>=5'
904+ peerDependenciesMeta:
905+ '@aws-sdk/client-rds-data':
906+ optional: true
907+ '@cloudflare/workers-types':
908+ optional: true
909+ '@electric-sql/pglite':
910+ optional: true
911+ '@libsql/client':
912+ optional: true
913+ '@libsql/client-wasm':
914+ optional: true
915+ '@neondatabase/serverless':
916+ optional: true
917+ '@op-engineering/op-sqlite':
918+ optional: true
919+ '@opentelemetry/api':
920+ optional: true
921+ '@planetscale/database':
922+ optional: true
923+ '@prisma/client':
924+ optional: true
925+ '@tidbcloud/serverless':
926+ optional: true
927+ '@types/better-sqlite3':
928+ optional: true
929+ '@types/pg':
930+ optional: true
931+ '@types/sql.js':
932+ optional: true
933+ '@upstash/redis':
934+ optional: true
935+ '@vercel/postgres':
936+ optional: true
937+ '@xata.io/client':
938+ optional: true
939+ better-sqlite3:
940+ optional: true
941+ bun-types:
942+ optional: true
943+ expo-sqlite:
944+ optional: true
945+ gel:
946+ optional: true
947+ knex:
948+ optional: true
949+ kysely:
950+ optional: true
951+ mysql2:
952+ optional: true
953+ pg:
954+ optional: true
955+ postgres:
956+ optional: true
957+ prisma:
958+ optional: true
959+ sql.js:
960+ optional: true
961+ sqlite3:
962+ optional: true
963+
964+ es-module-lexer@1.7.0:
965+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
966+
967+ esbuild@0.18.20:
968+ resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
969+ engines: {node: '>=12'}
970+ hasBin: true
971+
972+ esbuild@0.25.12:
973+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
974+ engines: {node: '>=18'}
975+ hasBin: true
976+
977+ esbuild@0.28.2:
978+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
979+ engines: {node: '>=18'}
980+ hasBin: true
981+
982+ estree-walker@3.0.3:
983+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
984+
985+ expect-type@1.4.0:
986+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
987+ engines: {node: '>=12.0.0'}
988+
989+ fast-xml-builder@1.3.1:
990+ resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==}
991+
992+ fast-xml-parser@5.11.1:
993+ resolution: {integrity: sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==}
994+ hasBin: true
995+
996+ fdir@6.5.0:
997+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
998+ engines: {node: '>=12.0.0'}
999+ peerDependencies:
1000+ picomatch: ^3 || ^4
1001+ peerDependenciesMeta:
1002+ picomatch:
1003+ optional: true
1004+
1005+ fsevents@2.3.3:
1006+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1007+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
1008+ os: [darwin]
1009+
1010+ get-tsconfig@4.14.3:
1011+ resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==}
1012+
1013+ is-unsafe@2.0.2:
1014+ resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==}
1015+
1016+ js-tokens@9.0.1:
1017+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
1018+
1019+ loupe@3.2.1:
1020+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
1021+
1022+ magic-string@0.30.21:
1023+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
1024+
1025+ ms@2.1.3:
1026+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1027+
1028+ nanoid@3.3.18:
1029+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
1030+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1031+ hasBin: true
1032+
1033+ on-exit-leak-free@2.1.2:
1034+ resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
1035+ engines: {node: '>=14.0.0'}
1036+
1037+ path-expression-matcher@1.6.2:
1038+ resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==}
1039+ engines: {node: '>=14.0.0'}
1040+
1041+ pathe@2.0.3:
1042+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
1043+
1044+ pathval@2.0.1:
1045+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
1046+ engines: {node: '>= 14.16'}
1047+
1048+ picocolors@1.1.1:
1049+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1050+
1051+ picomatch@4.0.7:
1052+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
1053+ engines: {node: '>=12'}
1054+
1055+ pino-abstract-transport@2.0.0:
1056+ resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
1057+
1058+ pino-std-serializers@7.1.0:
1059+ resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
1060+
1061+ pino@9.14.0:
1062+ resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==}
1063+ hasBin: true
1064+
1065+ postcss@8.5.28:
1066+ resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
1067+ engines: {node: ^10 || ^12 || >=14}
1068+
1069+ postgres@3.4.9:
1070+ resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
1071+ engines: {node: '>=12'}
1072+
1073+ process-warning@5.1.0:
1074+ resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
1075+
1076+ quick-format-unescaped@4.0.4:
1077+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
1078+
1079+ real-require@0.2.0:
1080+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
1081+ engines: {node: '>= 12.13.0'}
1082+
1083+ resolve-pkg-maps@1.0.0:
1084+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1085+
1086+ rollup@4.63.1:
1087+ resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==}
1088+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1089+ hasBin: true
1090+
1091+ safe-stable-stringify@2.5.0:
1092+ resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
1093+ engines: {node: '>=10'}
1094+
1095+ siginfo@2.0.0:
1096+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
1097+
1098+ sonic-boom@4.2.1:
1099+ resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
1100+
1101+ source-map-js@1.2.1:
1102+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1103+ engines: {node: '>=0.10.0'}
1104+
1105+ source-map-support@0.5.21:
1106+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
1107+
1108+ source-map@0.6.1:
1109+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
1110+ engines: {node: '>=0.10.0'}
1111+
1112+ split2@4.2.0:
1113+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
1114+ engines: {node: '>= 10.x'}
1115+
1116+ stackback@0.0.2:
1117+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
1118+
1119+ std-env@3.10.0:
1120+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
1121+
1122+ strip-literal@3.1.0:
1123+ resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
1124+
1125+ strnum@2.4.2:
1126+ resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==}
1127+
1128+ thread-stream@3.2.0:
1129+ resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==}
1130+
1131+ tinybench@2.9.0:
1132+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
1133+
1134+ tinyexec@0.3.2:
1135+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
1136+
1137+ tinyglobby@0.2.17:
1138+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
1139+ engines: {node: '>=12.0.0'}
1140+
1141+ tinypool@1.1.1:
1142+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
1143+ engines: {node: ^18.0.0 || >=20.0.0}
1144+
1145+ tinyrainbow@2.0.0:
1146+ resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
1147+ engines: {node: '>=14.0.0'}
1148+
1149+ tinyspy@4.0.6:
1150+ resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==}
1151+ engines: {node: '>=14.0.0'}
1152+
1153+ tsx@4.23.13:
1154+ resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==}
1155+ engines: {node: '>=18.0.0'}
1156+ hasBin: true
1157+
1158+ typescript@5.9.3:
1159+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
1160+ engines: {node: '>=14.17'}
1161+ hasBin: true
1162+
1163+ undici-types@7.18.2:
1164+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
1165+
1166+ vite-node@3.2.4:
1167+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
1168+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
1169+ hasBin: true
1170+
1171+ vite@7.3.6:
1172+ resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
1173+ engines: {node: ^20.19.0 || >=22.12.0}
1174+ hasBin: true
1175+ peerDependencies:
1176+ '@types/node': ^20.19.0 || >=22.12.0
1177+ jiti: '>=1.21.0'
1178+ less: ^4.0.0
1179+ lightningcss: ^1.21.0
1180+ sass: ^1.70.0
1181+ sass-embedded: ^1.70.0
1182+ stylus: '>=0.54.8'
1183+ sugarss: ^5.0.0
1184+ terser: ^5.16.0
1185+ tsx: ^4.8.1
1186+ yaml: ^2.4.2
1187+ peerDependenciesMeta:
1188+ '@types/node':
1189+ optional: true
1190+ jiti:
1191+ optional: true
1192+ less:
1193+ optional: true
1194+ lightningcss:
1195+ optional: true
1196+ sass:
1197+ optional: true
1198+ sass-embedded:
1199+ optional: true
1200+ stylus:
1201+ optional: true
1202+ sugarss:
1203+ optional: true
1204+ terser:
1205+ optional: true
1206+ tsx:
1207+ optional: true
1208+ yaml:
1209+ optional: true
1210+
1211+ vitest@3.2.7:
1212+ resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==}
1213+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
1214+ hasBin: true
1215+ peerDependencies:
1216+ '@edge-runtime/vm': '*'
1217+ '@types/debug': ^4.1.12
1218+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
1219+ '@vitest/browser': 3.2.7
1220+ '@vitest/ui': 3.2.7
1221+ happy-dom: '*'
1222+ jsdom: '*'
1223+ peerDependenciesMeta:
1224+ '@edge-runtime/vm':
1225+ optional: true
1226+ '@types/debug':
1227+ optional: true
1228+ '@types/node':
1229+ optional: true
1230+ '@vitest/browser':
1231+ optional: true
1232+ '@vitest/ui':
1233+ optional: true
1234+ happy-dom:
1235+ optional: true
1236+ jsdom:
1237+ optional: true
1238+
1239+ why-is-node-running@2.3.0:
1240+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
1241+ engines: {node: '>=8'}
1242+ hasBin: true
1243+
1244+ xml-naming@0.3.0:
1245+ resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
1246+ engines: {node: '>=16.0.0'}
1247+
1248+ yaml@2.9.0:
1249+ resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
1250+ engines: {node: '>= 14.6'}
1251+ hasBin: true
1252+
1253+ zod@4.5.4:
1254+ resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==}
1255+
1256+snapshots:
1257+
1258+ '@drizzle-team/brocli@0.10.2': {}
1259+
1260+ '@esbuild-kit/core-utils@3.3.2':
1261+ dependencies:
1262+ esbuild: 0.18.20
1263+ source-map-support: 0.5.21
1264+
1265+ '@esbuild-kit/esm-loader@2.6.5':
1266+ dependencies:
1267+ '@esbuild-kit/core-utils': 3.3.2
1268+ get-tsconfig: 4.14.3
1269+
1270+ '@esbuild/aix-ppc64@0.25.12':
1271+ optional: true
1272+
1273+ '@esbuild/aix-ppc64@0.28.2':
1274+ optional: true
1275+
1276+ '@esbuild/android-arm64@0.18.20':
1277+ optional: true
1278+
1279+ '@esbuild/android-arm64@0.25.12':
1280+ optional: true
1281+
1282+ '@esbuild/android-arm64@0.28.2':
1283+ optional: true
1284+
1285+ '@esbuild/android-arm@0.18.20':
1286+ optional: true
1287+
1288+ '@esbuild/android-arm@0.25.12':
1289+ optional: true
1290+
1291+ '@esbuild/android-arm@0.28.2':
1292+ optional: true
1293+
1294+ '@esbuild/android-x64@0.18.20':
1295+ optional: true
1296+
1297+ '@esbuild/android-x64@0.25.12':
1298+ optional: true
1299+
1300+ '@esbuild/android-x64@0.28.2':
1301+ optional: true
1302+
1303+ '@esbuild/darwin-arm64@0.18.20':
1304+ optional: true
1305+
1306+ '@esbuild/darwin-arm64@0.25.12':
1307+ optional: true
1308+
1309+ '@esbuild/darwin-arm64@0.28.2':
1310+ optional: true
1311+
1312+ '@esbuild/darwin-x64@0.18.20':
1313+ optional: true
1314+
1315+ '@esbuild/darwin-x64@0.25.12':
1316+ optional: true
1317+
1318+ '@esbuild/darwin-x64@0.28.2':
1319+ optional: true
1320+
1321+ '@esbuild/freebsd-arm64@0.18.20':
1322+ optional: true
1323+
1324+ '@esbuild/freebsd-arm64@0.25.12':
1325+ optional: true
1326+
1327+ '@esbuild/freebsd-arm64@0.28.2':
1328+ optional: true
1329+
1330+ '@esbuild/freebsd-x64@0.18.20':
1331+ optional: true
1332+
1333+ '@esbuild/freebsd-x64@0.25.12':
1334+ optional: true
1335+
1336+ '@esbuild/freebsd-x64@0.28.2':
1337+ optional: true
1338+
1339+ '@esbuild/linux-arm64@0.18.20':
1340+ optional: true
1341+
1342+ '@esbuild/linux-arm64@0.25.12':
1343+ optional: true
1344+
1345+ '@esbuild/linux-arm64@0.28.2':
1346+ optional: true
1347+
1348+ '@esbuild/linux-arm@0.18.20':
1349+ optional: true
1350+
1351+ '@esbuild/linux-arm@0.25.12':
1352+ optional: true
1353+
1354+ '@esbuild/linux-arm@0.28.2':
1355+ optional: true
1356+
1357+ '@esbuild/linux-ia32@0.18.20':
1358+ optional: true
1359+
1360+ '@esbuild/linux-ia32@0.25.12':
1361+ optional: true
1362+
1363+ '@esbuild/linux-ia32@0.28.2':
1364+ optional: true
1365+
1366+ '@esbuild/linux-loong64@0.18.20':
1367+ optional: true
1368+
1369+ '@esbuild/linux-loong64@0.25.12':
1370+ optional: true
1371+
1372+ '@esbuild/linux-loong64@0.28.2':
1373+ optional: true
1374+
1375+ '@esbuild/linux-mips64el@0.18.20':
1376+ optional: true
1377+
1378+ '@esbuild/linux-mips64el@0.25.12':
1379+ optional: true
1380+
1381+ '@esbuild/linux-mips64el@0.28.2':
1382+ optional: true
1383+
1384+ '@esbuild/linux-ppc64@0.18.20':
1385+ optional: true
1386+
1387+ '@esbuild/linux-ppc64@0.25.12':
1388+ optional: true
1389+
1390+ '@esbuild/linux-ppc64@0.28.2':
1391+ optional: true
1392+
1393+ '@esbuild/linux-riscv64@0.18.20':
1394+ optional: true
1395+
1396+ '@esbuild/linux-riscv64@0.25.12':
1397+ optional: true
1398+
1399+ '@esbuild/linux-riscv64@0.28.2':
1400+ optional: true
1401+
1402+ '@esbuild/linux-s390x@0.18.20':
1403+ optional: true
1404+
1405+ '@esbuild/linux-s390x@0.25.12':
1406+ optional: true
1407+
1408+ '@esbuild/linux-s390x@0.28.2':
1409+ optional: true
1410+
1411+ '@esbuild/linux-x64@0.18.20':
1412+ optional: true
1413+
1414+ '@esbuild/linux-x64@0.25.12':
1415+ optional: true
1416+
1417+ '@esbuild/linux-x64@0.28.2':
1418+ optional: true
1419+
1420+ '@esbuild/netbsd-arm64@0.25.12':
1421+ optional: true
1422+
1423+ '@esbuild/netbsd-arm64@0.28.2':
1424+ optional: true
1425+
1426+ '@esbuild/netbsd-x64@0.18.20':
1427+ optional: true
1428+
1429+ '@esbuild/netbsd-x64@0.25.12':
1430+ optional: true
1431+
1432+ '@esbuild/netbsd-x64@0.28.2':
1433+ optional: true
1434+
1435+ '@esbuild/openbsd-arm64@0.25.12':
1436+ optional: true
1437+
1438+ '@esbuild/openbsd-arm64@0.28.2':
1439+ optional: true
1440+
1441+ '@esbuild/openbsd-x64@0.18.20':
1442+ optional: true
1443+
1444+ '@esbuild/openbsd-x64@0.25.12':
1445+ optional: true
1446+
1447+ '@esbuild/openbsd-x64@0.28.2':
1448+ optional: true
1449+
1450+ '@esbuild/openharmony-arm64@0.25.12':
1451+ optional: true
1452+
1453+ '@esbuild/openharmony-arm64@0.28.2':
1454+ optional: true
1455+
1456+ '@esbuild/sunos-x64@0.18.20':
1457+ optional: true
1458+
1459+ '@esbuild/sunos-x64@0.25.12':
1460+ optional: true
1461+
1462+ '@esbuild/sunos-x64@0.28.2':
1463+ optional: true
1464+
1465+ '@esbuild/win32-arm64@0.18.20':
1466+ optional: true
1467+
1468+ '@esbuild/win32-arm64@0.25.12':
1469+ optional: true
1470+
1471+ '@esbuild/win32-arm64@0.28.2':
1472+ optional: true
1473+
1474+ '@esbuild/win32-ia32@0.18.20':
1475+ optional: true
1476+
1477+ '@esbuild/win32-ia32@0.25.12':
1478+ optional: true
1479+
1480+ '@esbuild/win32-ia32@0.28.2':
1481+ optional: true
1482+
1483+ '@esbuild/win32-x64@0.18.20':
1484+ optional: true
1485+
1486+ '@esbuild/win32-x64@0.25.12':
1487+ optional: true
1488+
1489+ '@esbuild/win32-x64@0.28.2':
1490+ optional: true
1491+
1492+ '@jridgewell/sourcemap-codec@1.6.0': {}
1493+
1494+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
1495+ optional: true
1496+
1497+ '@nodable/entities@3.0.0': {}
1498+
1499+ '@pinojs/redact@0.4.0': {}
1500+
1501+ '@rollup/rollup-android-arm-eabi@4.63.1':
1502+ optional: true
1503+
1504+ '@rollup/rollup-android-arm64@4.63.1':
1505+ optional: true
1506+
1507+ '@rollup/rollup-darwin-arm64@4.63.1':
1508+ optional: true
1509+
1510+ '@rollup/rollup-darwin-x64@4.63.1':
1511+ optional: true
1512+
1513+ '@rollup/rollup-freebsd-arm64@4.63.1':
1514+ optional: true
1515+
1516+ '@rollup/rollup-freebsd-x64@4.63.1':
1517+ optional: true
1518+
1519+ '@rollup/rollup-linux-arm-gnueabihf@4.63.1':
1520+ optional: true
1521+
1522+ '@rollup/rollup-linux-arm-musleabihf@4.63.1':
1523+ optional: true
1524+
1525+ '@rollup/rollup-linux-arm64-gnu@4.63.1':
1526+ optional: true
1527+
1528+ '@rollup/rollup-linux-arm64-musl@4.63.1':
1529+ optional: true
1530+
1531+ '@rollup/rollup-linux-loong64-gnu@4.63.1':
1532+ optional: true
1533+
1534+ '@rollup/rollup-linux-loong64-musl@4.63.1':
1535+ optional: true
1536+
1537+ '@rollup/rollup-linux-ppc64-gnu@4.63.1':
1538+ optional: true
1539+
1540+ '@rollup/rollup-linux-ppc64-musl@4.63.1':
1541+ optional: true
1542+
1543+ '@rollup/rollup-linux-riscv64-gnu@4.63.1':
1544+ optional: true
1545+
1546+ '@rollup/rollup-linux-riscv64-musl@4.63.1':
1547+ optional: true
1548+
1549+ '@rollup/rollup-linux-s390x-gnu@4.63.1':
1550+ optional: true
1551+
1552+ '@rollup/rollup-linux-x64-gnu@4.63.1':
1553+ optional: true
1554+
1555+ '@rollup/rollup-linux-x64-musl@4.63.1':
1556+ optional: true
1557+
1558+ '@rollup/rollup-openbsd-x64@4.63.1':
1559+ optional: true
1560+
1561+ '@rollup/rollup-openharmony-arm64@4.63.1':
1562+ optional: true
1563+
1564+ '@rollup/rollup-win32-arm64-msvc@4.63.1':
1565+ optional: true
1566+
1567+ '@rollup/rollup-win32-ia32-msvc@4.63.1':
1568+ optional: true
1569+
1570+ '@rollup/rollup-win32-x64-gnu@4.63.1':
1571+ optional: true
1572+
1573+ '@rollup/rollup-win32-x64-msvc@4.63.1':
1574+ optional: true
1575+
1576+ '@types/chai@5.2.3':
1577+ dependencies:
1578+ '@types/deep-eql': 4.0.2
1579+ assertion-error: 2.0.1
1580+
1581+ '@types/deep-eql@4.0.2': {}
1582+
1583+ '@types/estree@1.0.9': {}
1584+
1585+ '@types/node@24.13.3':
1586+ dependencies:
1587+ undici-types: 7.18.2
1588+
1589+ '@vitest/expect@3.2.7':
1590+ dependencies:
1591+ '@types/chai': 5.2.3
1592+ '@vitest/spy': 3.2.7
1593+ '@vitest/utils': 3.2.7
1594+ chai: 5.3.3
1595+ tinyrainbow: 2.0.0
1596+
1597+ '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0))':
1598+ dependencies:
1599+ '@vitest/spy': 3.2.7
1600+ estree-walker: 3.0.3
1601+ magic-string: 0.30.21
1602+ optionalDependencies:
1603+ vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
1604+
1605+ '@vitest/pretty-format@3.2.7':
1606+ dependencies:
1607+ tinyrainbow: 2.0.0
1608+
1609+ '@vitest/runner@3.2.7':
1610+ dependencies:
1611+ '@vitest/utils': 3.2.7
1612+ pathe: 2.0.3
1613+ strip-literal: 3.1.0
1614+
1615+ '@vitest/snapshot@3.2.7':
1616+ dependencies:
1617+ '@vitest/pretty-format': 3.2.7
1618+ magic-string: 0.30.21
1619+ pathe: 2.0.3
1620+
1621+ '@vitest/spy@3.2.7':
1622+ dependencies:
1623+ tinyspy: 4.0.6
1624+
1625+ '@vitest/utils@3.2.7':
1626+ dependencies:
1627+ '@vitest/pretty-format': 3.2.7
1628+ loupe: 3.2.1
1629+ tinyrainbow: 2.0.0
1630+
1631+ anynum@1.0.1: {}
1632+
1633+ assertion-error@2.0.1: {}
1634+
1635+ atomic-sleep@1.0.0: {}
1636+
1637+ buffer-from@1.1.2: {}
1638+
1639+ cac@6.7.14: {}
1640+
1641+ chai@5.3.3:
1642+ dependencies:
1643+ assertion-error: 2.0.1
1644+ check-error: 2.1.3
1645+ deep-eql: 5.0.2
1646+ loupe: 3.2.1
1647+ pathval: 2.0.1
1648+
1649+ check-error@2.1.3: {}
1650+
1651+ debug@4.4.3:
1652+ dependencies:
1653+ ms: 2.1.3
1654+
1655+ deep-eql@5.0.2: {}
1656+
1657+ drizzle-kit@0.31.10:
1658+ dependencies:
1659+ '@drizzle-team/brocli': 0.10.2
1660+ '@esbuild-kit/esm-loader': 2.6.5
1661+ esbuild: 0.25.12
1662+ tsx: 4.23.13
1663+
1664+ drizzle-orm@0.45.2(postgres@3.4.9):
1665+ optionalDependencies:
1666+ postgres: 3.4.9
1667+
1668+ es-module-lexer@1.7.0: {}
1669+
1670+ esbuild@0.18.20:
1671+ optionalDependencies:
1672+ '@esbuild/android-arm': 0.18.20
1673+ '@esbuild/android-arm64': 0.18.20
1674+ '@esbuild/android-x64': 0.18.20
1675+ '@esbuild/darwin-arm64': 0.18.20
1676+ '@esbuild/darwin-x64': 0.18.20
1677+ '@esbuild/freebsd-arm64': 0.18.20
1678+ '@esbuild/freebsd-x64': 0.18.20
1679+ '@esbuild/linux-arm': 0.18.20
1680+ '@esbuild/linux-arm64': 0.18.20
1681+ '@esbuild/linux-ia32': 0.18.20
1682+ '@esbuild/linux-loong64': 0.18.20
1683+ '@esbuild/linux-mips64el': 0.18.20
1684+ '@esbuild/linux-ppc64': 0.18.20
1685+ '@esbuild/linux-riscv64': 0.18.20
1686+ '@esbuild/linux-s390x': 0.18.20
1687+ '@esbuild/linux-x64': 0.18.20
1688+ '@esbuild/netbsd-x64': 0.18.20
1689+ '@esbuild/openbsd-x64': 0.18.20
1690+ '@esbuild/sunos-x64': 0.18.20
1691+ '@esbuild/win32-arm64': 0.18.20
1692+ '@esbuild/win32-ia32': 0.18.20
1693+ '@esbuild/win32-x64': 0.18.20
1694+
1695+ esbuild@0.25.12:
1696+ optionalDependencies:
1697+ '@esbuild/aix-ppc64': 0.25.12
1698+ '@esbuild/android-arm': 0.25.12
1699+ '@esbuild/android-arm64': 0.25.12
1700+ '@esbuild/android-x64': 0.25.12
1701+ '@esbuild/darwin-arm64': 0.25.12
1702+ '@esbuild/darwin-x64': 0.25.12
1703+ '@esbuild/freebsd-arm64': 0.25.12
1704+ '@esbuild/freebsd-x64': 0.25.12
1705+ '@esbuild/linux-arm': 0.25.12
1706+ '@esbuild/linux-arm64': 0.25.12
1707+ '@esbuild/linux-ia32': 0.25.12
1708+ '@esbuild/linux-loong64': 0.25.12
1709+ '@esbuild/linux-mips64el': 0.25.12
1710+ '@esbuild/linux-ppc64': 0.25.12
1711+ '@esbuild/linux-riscv64': 0.25.12
1712+ '@esbuild/linux-s390x': 0.25.12
1713+ '@esbuild/linux-x64': 0.25.12
1714+ '@esbuild/netbsd-arm64': 0.25.12
1715+ '@esbuild/netbsd-x64': 0.25.12
1716+ '@esbuild/openbsd-arm64': 0.25.12
1717+ '@esbuild/openbsd-x64': 0.25.12
1718+ '@esbuild/openharmony-arm64': 0.25.12
1719+ '@esbuild/sunos-x64': 0.25.12
1720+ '@esbuild/win32-arm64': 0.25.12
1721+ '@esbuild/win32-ia32': 0.25.12
1722+ '@esbuild/win32-x64': 0.25.12
1723+
1724+ esbuild@0.28.2:
1725+ optionalDependencies:
1726+ '@esbuild/aix-ppc64': 0.28.2
1727+ '@esbuild/android-arm': 0.28.2
1728+ '@esbuild/android-arm64': 0.28.2
1729+ '@esbuild/android-x64': 0.28.2
1730+ '@esbuild/darwin-arm64': 0.28.2
1731+ '@esbuild/darwin-x64': 0.28.2
1732+ '@esbuild/freebsd-arm64': 0.28.2
1733+ '@esbuild/freebsd-x64': 0.28.2
1734+ '@esbuild/linux-arm': 0.28.2
1735+ '@esbuild/linux-arm64': 0.28.2
1736+ '@esbuild/linux-ia32': 0.28.2
1737+ '@esbuild/linux-loong64': 0.28.2
1738+ '@esbuild/linux-mips64el': 0.28.2
1739+ '@esbuild/linux-ppc64': 0.28.2
1740+ '@esbuild/linux-riscv64': 0.28.2
1741+ '@esbuild/linux-s390x': 0.28.2
1742+ '@esbuild/linux-x64': 0.28.2
1743+ '@esbuild/netbsd-arm64': 0.28.2
1744+ '@esbuild/netbsd-x64': 0.28.2
1745+ '@esbuild/openbsd-arm64': 0.28.2
1746+ '@esbuild/openbsd-x64': 0.28.2
1747+ '@esbuild/openharmony-arm64': 0.28.2
1748+ '@esbuild/sunos-x64': 0.28.2
1749+ '@esbuild/win32-arm64': 0.28.2
1750+ '@esbuild/win32-ia32': 0.28.2
1751+ '@esbuild/win32-x64': 0.28.2
1752+
1753+ estree-walker@3.0.3:
1754+ dependencies:
1755+ '@types/estree': 1.0.9
1756+
1757+ expect-type@1.4.0: {}
1758+
1759+ fast-xml-builder@1.3.1:
1760+ dependencies:
1761+ path-expression-matcher: 1.6.2
1762+ xml-naming: 0.3.0
1763+
1764+ fast-xml-parser@5.11.1:
1765+ dependencies:
1766+ '@nodable/entities': 3.0.0
1767+ fast-xml-builder: 1.3.1
1768+ is-unsafe: 2.0.2
1769+ path-expression-matcher: 1.6.2
1770+ strnum: 2.4.2
1771+ xml-naming: 0.3.0
1772+
1773+ fdir@6.5.0(picomatch@4.0.7):
1774+ optionalDependencies:
1775+ picomatch: 4.0.7
1776+
1777+ fsevents@2.3.3:
1778+ optional: true
1779+
1780+ get-tsconfig@4.14.3:
1781+ dependencies:
1782+ resolve-pkg-maps: 1.0.0
1783+
1784+ is-unsafe@2.0.2: {}
1785+
1786+ js-tokens@9.0.1: {}
1787+
1788+ loupe@3.2.1: {}
1789+
1790+ magic-string@0.30.21:
1791+ dependencies:
1792+ '@jridgewell/sourcemap-codec': 1.6.0
1793+
1794+ ms@2.1.3: {}
1795+
1796+ nanoid@3.3.18: {}
1797+
1798+ on-exit-leak-free@2.1.2: {}
1799+
1800+ path-expression-matcher@1.6.2: {}
1801+
1802+ pathe@2.0.3: {}
1803+
1804+ pathval@2.0.1: {}
1805+
1806+ picocolors@1.1.1: {}
1807+
1808+ picomatch@4.0.7: {}
1809+
1810+ pino-abstract-transport@2.0.0:
1811+ dependencies:
1812+ split2: 4.2.0
1813+
1814+ pino-std-serializers@7.1.0: {}
1815+
1816+ pino@9.14.0:
1817+ dependencies:
1818+ '@pinojs/redact': 0.4.0
1819+ atomic-sleep: 1.0.0
1820+ on-exit-leak-free: 2.1.2
1821+ pino-abstract-transport: 2.0.0
1822+ pino-std-serializers: 7.1.0
1823+ process-warning: 5.1.0
1824+ quick-format-unescaped: 4.0.4
1825+ real-require: 0.2.0
1826+ safe-stable-stringify: 2.5.0
1827+ sonic-boom: 4.2.1
1828+ thread-stream: 3.2.0
1829+
1830+ postcss@8.5.28:
1831+ dependencies:
1832+ nanoid: 3.3.18
1833+ picocolors: 1.1.1
1834+ source-map-js: 1.2.1
1835+
1836+ postgres@3.4.9: {}
1837+
1838+ process-warning@5.1.0: {}
1839+
1840+ quick-format-unescaped@4.0.4: {}
1841+
1842+ real-require@0.2.0: {}
1843+
1844+ resolve-pkg-maps@1.0.0: {}
1845+
1846+ rollup@4.63.1:
1847+ dependencies:
1848+ '@types/estree': 1.0.9
1849+ optionalDependencies:
1850+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
1851+ '@rollup/rollup-android-arm-eabi': 4.63.1
1852+ '@rollup/rollup-android-arm64': 4.63.1
1853+ '@rollup/rollup-darwin-arm64': 4.63.1
1854+ '@rollup/rollup-darwin-x64': 4.63.1
1855+ '@rollup/rollup-freebsd-arm64': 4.63.1
1856+ '@rollup/rollup-freebsd-x64': 4.63.1
1857+ '@rollup/rollup-linux-arm-gnueabihf': 4.63.1
1858+ '@rollup/rollup-linux-arm-musleabihf': 4.63.1
1859+ '@rollup/rollup-linux-arm64-gnu': 4.63.1
1860+ '@rollup/rollup-linux-arm64-musl': 4.63.1
1861+ '@rollup/rollup-linux-loong64-gnu': 4.63.1
1862+ '@rollup/rollup-linux-loong64-musl': 4.63.1
1863+ '@rollup/rollup-linux-ppc64-gnu': 4.63.1
1864+ '@rollup/rollup-linux-ppc64-musl': 4.63.1
1865+ '@rollup/rollup-linux-riscv64-gnu': 4.63.1
1866+ '@rollup/rollup-linux-riscv64-musl': 4.63.1
1867+ '@rollup/rollup-linux-s390x-gnu': 4.63.1
1868+ '@rollup/rollup-linux-x64-gnu': 4.63.1
1869+ '@rollup/rollup-linux-x64-musl': 4.63.1
1870+ '@rollup/rollup-openbsd-x64': 4.63.1
1871+ '@rollup/rollup-openharmony-arm64': 4.63.1
1872+ '@rollup/rollup-win32-arm64-msvc': 4.63.1
1873+ '@rollup/rollup-win32-ia32-msvc': 4.63.1
1874+ '@rollup/rollup-win32-x64-gnu': 4.63.1
1875+ '@rollup/rollup-win32-x64-msvc': 4.63.1
1876+ fsevents: 2.3.3
1877+
1878+ safe-stable-stringify@2.5.0: {}
1879+
1880+ siginfo@2.0.0: {}
1881+
1882+ sonic-boom@4.2.1:
1883+ dependencies:
1884+ atomic-sleep: 1.0.0
1885+
1886+ source-map-js@1.2.1: {}
1887+
1888+ source-map-support@0.5.21:
1889+ dependencies:
1890+ buffer-from: 1.1.2
1891+ source-map: 0.6.1
1892+
1893+ source-map@0.6.1: {}
1894+
1895+ split2@4.2.0: {}
1896+
1897+ stackback@0.0.2: {}
1898+
1899+ std-env@3.10.0: {}
1900+
1901+ strip-literal@3.1.0:
1902+ dependencies:
1903+ js-tokens: 9.0.1
1904+
1905+ strnum@2.4.2:
1906+ dependencies:
1907+ anynum: 1.0.1
1908+
1909+ thread-stream@3.2.0:
1910+ dependencies:
1911+ real-require: 0.2.0
1912+
1913+ tinybench@2.9.0: {}
1914+
1915+ tinyexec@0.3.2: {}
1916+
1917+ tinyglobby@0.2.17:
1918+ dependencies:
1919+ fdir: 6.5.0(picomatch@4.0.7)
1920+ picomatch: 4.0.7
1921+
1922+ tinypool@1.1.1: {}
1923+
1924+ tinyrainbow@2.0.0: {}
1925+
1926+ tinyspy@4.0.6: {}
1927+
1928+ tsx@4.23.13:
1929+ dependencies:
1930+ esbuild: 0.28.2
1931+ optionalDependencies:
1932+ fsevents: 2.3.3
1933+
1934+ typescript@5.9.3: {}
1935+
1936+ undici-types@7.18.2: {}
1937+
1938+ vite-node@3.2.4(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0):
1939+ dependencies:
1940+ cac: 6.7.14
1941+ debug: 4.4.3
1942+ es-module-lexer: 1.7.0
1943+ pathe: 2.0.3
1944+ vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
1945+ transitivePeerDependencies:
1946+ - '@types/node'
1947+ - jiti
1948+ - less
1949+ - lightningcss
1950+ - sass
1951+ - sass-embedded
1952+ - stylus
1953+ - sugarss
1954+ - supports-color
1955+ - terser
1956+ - tsx
1957+ - yaml
1958+
1959+ vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0):
1960+ dependencies:
1961+ esbuild: 0.28.2
1962+ fdir: 6.5.0(picomatch@4.0.7)
1963+ picomatch: 4.0.7
1964+ postcss: 8.5.28
1965+ rollup: 4.63.1
1966+ tinyglobby: 0.2.17
1967+ optionalDependencies:
1968+ '@types/node': 24.13.3
1969+ fsevents: 2.3.3
1970+ tsx: 4.23.13
1971+ yaml: 2.9.0
1972+
1973+ vitest@3.2.7(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0):
1974+ dependencies:
1975+ '@types/chai': 5.2.3
1976+ '@vitest/expect': 3.2.7
1977+ '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0))
1978+ '@vitest/pretty-format': 3.2.7
1979+ '@vitest/runner': 3.2.7
1980+ '@vitest/snapshot': 3.2.7
1981+ '@vitest/spy': 3.2.7
1982+ '@vitest/utils': 3.2.7
1983+ chai: 5.3.3
1984+ debug: 4.4.3
1985+ expect-type: 1.4.0
1986+ magic-string: 0.30.21
1987+ pathe: 2.0.3
1988+ picomatch: 4.0.7
1989+ std-env: 3.10.0
1990+ tinybench: 2.9.0
1991+ tinyexec: 0.3.2
1992+ tinyglobby: 0.2.17
1993+ tinypool: 1.1.1
1994+ tinyrainbow: 2.0.0
1995+ vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
1996+ vite-node: 3.2.4(@types/node@24.13.3)(tsx@4.23.13)(yaml@2.9.0)
1997+ why-is-node-running: 2.3.0
1998+ optionalDependencies:
1999+ '@types/node': 24.13.3
2000+ transitivePeerDependencies:
2001+ - jiti
2002+ - less
2003+ - lightningcss
2004+ - msw
2005+ - sass
2006+ - sass-embedded
2007+ - stylus
2008+ - sugarss
2009+ - supports-color
2010+ - terser
2011+ - tsx
2012+ - yaml
2013+
2014+ why-is-node-running@2.3.0:
2015+ dependencies:
2016+ siginfo: 2.0.0
2017+ stackback: 0.0.2
2018+
2019+ xml-naming@0.3.0: {}
2020+
2021+ yaml@2.9.0: {}
2022+
2023+ zod@4.5.4: {}
added pnpm-workspace.yaml +4 −0
@@ -0,0 +1,4 @@
1+packages:
2+ - apps/*
3+ - packages/*
4+ - workers
added scripts/ci.ts +136 −0
@@ -0,0 +1,136 @@
1+#!/usr/bin/env tsx
2+/**
3+ * CancerIndex operator CLI.
4+ *
5+ * pnpm cix connectors list connectors + health
6+ * pnpm cix run <id> [--mode full|incremental|dry_run] [--max-records N] [--max-minutes M] [--reset-cursor]
7+ * pnpm cix run-all [--max-minutes M] run every active connector in registry order
8+ * pnpm cix health <id> source liveness probe
9+ * pnpm cix sources:sync manifests → sources table
10+ * pnpm cix counters recompute entity counters
11+ * pnpm cix rank [--scope …] recompute ranking snapshots
12+ * pnpm cix stats table counts
13+ * pnpm cix trace <table> <id> lineage of a value (rankings/epidemiology/frequency)
14+ */
15+import { existsSync } from 'node:fs';
16+import path from 'node:path';
17+import { sql } from 'drizzle-orm';
18+
19+for (const candidate of [path.resolve(process.cwd(), '.env')]) {
20+ if (existsSync(candidate)) {
21+ try {
22+ process.loadEnvFile(candidate);
23+ } catch {
24+ /* ignore */
25+ }
26+ }
27+}
28+
29+const [, , cmd, ...rest] = process.argv;
30+const flags: Record<string, string | boolean> = {};
31+const positional: string[] = [];
32+for (let i = 0; i < rest.length; i++) {
33+ const a = rest[i]!;
34+ if (a.startsWith('--')) {
35+ const k = a.slice(2);
36+ const next = rest[i + 1];
37+ if (next && !next.startsWith('--')) {
38+ flags[k] = next;
39+ i++;
40+ } else flags[k] = true;
41+ } else positional.push(a);
42+}
43+
44+async function main() {
45+ const { getDb, closeDb } = await import('@cancerindex/database');
46+ const db = getDb();
47+ try {
48+ switch (cmd) {
49+ case 'connectors': {
50+ const { CONNECTORS } = await import('@cancerindex/connectors');
51+ const rows = await db.execute<{ connector_id: string; health: string; last_success_at: string | null; cursor: unknown }>(sql`SELECT connector_id, health, last_success_at, cursor FROM connector_cursors`);
52+ const byId = new Map(rows.map((r) => [r.connector_id, r]));
53+ for (const c of CONNECTORS) {
54+ const h = byId.get(c.manifest.id);
55+ console.log(`${c.manifest.id.padEnd(18)} tier${c.manifest.tier} ${c.manifest.status.padEnd(20)} license=${c.manifest.licenseStatus.padEnd(10)} health=${(h?.health ?? 'never-run').padEnd(12)} last_ok=${h?.last_success_at ?? '-'}`);
56+ }
57+ break;
58+ }
59+ case 'sources:sync': {
60+ const { syncSources, PLANNED_MANIFESTS } = await import('@cancerindex/connectors');
61+ const r = await syncSources(db, PLANNED_MANIFESTS);
62+ console.log(`[sources] created=${r.created} updated=${r.updated}`);
63+ break;
64+ }
65+ case 'health': {
66+ const { getConnector, RunContext } = await import('@cancerindex/connectors');
67+ const c = getConnector(positional[0] ?? '');
68+ if (!c) throw new Error(`unknown connector ${positional[0]}`);
69+ const [src] = await db.execute<{ id: string }>(sql`SELECT id FROM sources WHERE slug = ${c.manifest.id}`);
70+ const ctx = new RunContext(db, c.manifest, src?.id ?? 'CI-SOURCE-00000000', 'probe', { maxMinutes: 1 }, 0);
71+ console.log(await c.healthCheck(ctx));
72+ break;
73+ }
74+ case 'run': {
75+ const { getConnector, runConnector } = await import('@cancerindex/connectors');
76+ const c = getConnector(positional[0] ?? '');
77+ if (!c) throw new Error(`unknown connector ${positional[0]}`);
78+ const r = await runConnector(db, c, {
79+ mode: (flags.mode as never) ?? undefined,
80+ maxRecords: flags['max-records'] ? Number(flags['max-records']) : undefined,
81+ maxMinutes: flags['max-minutes'] ? Number(flags['max-minutes']) : undefined,
82+ resetCursor: !!flags['reset-cursor'],
83+ });
84+ console.log(`[run] ${r.runId} ${r.status}`, r.counters);
85+ if (r.status === 'failed') process.exitCode = 1;
86+ break;
87+ }
88+ case 'run-all': {
89+ const { CONNECTORS, runConnector } = await import('@cancerindex/connectors');
90+ for (const c of CONNECTORS) {
91+ if (c.manifest.status !== 'active') {
92+ console.log(`[skip] ${c.manifest.id} (${c.manifest.status})`);
93+ continue;
94+ }
95+ const r = await runConnector(db, c, { maxMinutes: flags['max-minutes'] ? Number(flags['max-minutes']) : undefined });
96+ console.log(`[run] ${r.runId} ${r.status}`, r.counters);
97+ }
98+ break;
99+ }
100+ case 'counters': {
101+ const { refreshCounters } = await import('@cancerindex/ranking');
102+ const n = await refreshCounters(db);
103+ console.log(`[counters] refreshed ${n} entities`);
104+ break;
105+ }
106+ case 'rank': {
107+ const { computeAllRankings } = await import('@cancerindex/ranking');
108+ const res = await computeAllRankings(db);
109+ for (const r of res) console.log(`[rank] ${r.metricSlug} ${r.scopeKey} eligible=${r.eligible}`);
110+ break;
111+ }
112+ case 'stats': {
113+ const tables = ['sources', 'ingest_runs', 'source_records', 'provenance', 'cancers', 'cancer_aliases', 'cancer_hierarchy', 'cancer_codes', 'genes', 'variants', 'drugs', 'clinical_trials', 'trial_conditions', 'publications', 'literature_counts', 'civic_evidence_items', 'genomic_cohorts', 'cancer_gene_frequencies', 'epidemiology_observations', 'survival_observations', 'knowledge_edges', 'unresolved_labels', 'rankings', 'ranking_snapshots'];
114+ for (const t of tables) {
115+ const [r] = await db.execute<{ n: string }>(sql.raw(`SELECT count(*)::text AS n FROM ${t}`));
116+ console.log(`${t.padEnd(28)} ${r?.n}`);
117+ }
118+ break;
119+ }
120+ case 'trace': {
121+ const { traceValue } = await import('@cancerindex/ranking');
122+ console.log(JSON.stringify(await traceValue(db, positional[0] ?? '', positional[1] ?? ''), null, 2));
123+ break;
124+ }
125+ default:
126+ console.log('usage: pnpm cix <connectors|run|run-all|health|sources:sync|counters|rank|stats|trace>');
127+ }
128+ } finally {
129+ await closeDb();
130+ }
131+}
132+
133+main().catch((e) => {
134+ console.error(e);
135+ process.exit(1);
136+});
added tsconfig.base.json +22 −0
@@ -0,0 +1,22 @@
1+{
2+ "compilerOptions": {
3+ "target": "ES2023",
4+ "lib": ["ES2023"],
5+ "module": "NodeNext",
6+ "moduleResolution": "NodeNext",
7+ "strict": true,
8+ "noUncheckedIndexedAccess": true,
9+ "noImplicitOverride": true,
10+ "noFallthroughCasesInSwitch": true,
11+ "exactOptionalPropertyTypes": false,
12+ "esModuleInterop": true,
13+ "skipLibCheck": true,
14+ "resolveJsonModule": true,
15+ "declaration": true,
16+ "declarationMap": true,
17+ "sourceMap": true,
18+ "isolatedModules": true,
19+ "verbatimModuleSyntax": true,
20+ "forceConsistentCasingInFileNames": true
21+ }
22+}
added vitest.workspace.ts +1 −0
@@ -0,0 +1 @@
1+export default ['packages/*', 'apps/*', 'workers'];
2
Menu

Theme