SPB Git forge

spb/cancerindex

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

Web: align therapy names with slugs, cap evidence/variant lists, truncate descriptions; ranking eligibility; SDK field-stats COALESCE; resolver lineage/display/qualifier fallbacks

Simon-Pierre Boucher committed 16 days ago (Sep 8, 2026) parent 97e14ae

5 changed files +17 −14

modified apps/web/src/app/gene/[symbol]/page.tsx +1 −1
@@ -119,7 +119,7 @@ export default async function GenePage({ params }: { params: Promise<{ symbol: s
119 119 />
120 120 <Freshness dataUpdatedAt={g.updated_at} extra="source: hgnc" />
121 121 </Section>
122 − <Section id="variants" kicker="Variants" title={`Variants (${fmtInt(variants.length)})`} level={3}>
122 + <Section id="variants" kicker="Variants" title={variants.length >= 100 ? `Variants (top ${fmtInt(variants.length)} by evidence — full list via the API)` : `Variants (${fmtInt(variants.length)})`} level={3}>
123 123 {variants.length ? (
124 124 <ul className="max-h-[480px] overflow-y-auto text-[13.5px]">
125 125 {variants.map((v) => (
modified apps/web/src/components/cancer/tabs/drugs.tsx +4 −5
@@ -19,14 +19,13 @@ export async function DrugsTab({ b, jurisdiction }: { b: CancerBundle; jurisdict
19 19 // Therapies mentioned in curated evidence (not approvals) — listed separately, never called "approved".
20 20 const therapyMap = new Map<string, { slug: string; name: string; n: number; sensitivity: number; resistance: number }>();
21 21 for (const e of evidence) {
22 − e.therapy_ids.forEach((id, i) => {
23 − const slug = e.therapy_slugs?.[i];
24 − if (!slug) return;
25 − const cur = therapyMap.get(id) ?? { slug, name: e.therapy_names[i] ?? slug, n: 0, sensitivity: 0, resistance: 0 };
22 + // therapy_slugs and therapy_slug_names share one ORDER BY; therapy_ids/therapy_names follow CIViC's order and must not be zipped with them.
23 + (e.therapy_slugs ?? []).forEach((slug, i) => {
24 + const cur = therapyMap.get(slug) ?? { slug, name: e.therapy_slug_names?.[i] ?? slug, n: 0, sensitivity: 0, resistance: 0 };
26 25 cur.n += 1;
27 26 if ((e.significance ?? '').includes('SENSITIV')) cur.sensitivity += 1;
28 27 if ((e.significance ?? '').includes('RESIST')) cur.resistance += 1;
29 − therapyMap.set(id, cur);
28 + therapyMap.set(slug, cur);
30 29 });
31 30 }
32 31 const therapies = [...therapyMap.values()].sort((a, c) => c.n - a.n);
modified apps/web/src/components/data/evidence-table.tsx +2 −2
@@ -72,7 +72,7 @@ export function EvidenceTable({ items, prov, showCancer = false, showVariant = t
72 72 <span key={s}>
73 73 {j > 0 ? ' + ' : ''}
74 74 <Link className="ci-link" href={`/drug/${s}`}>
75 − {e.therapy_names[j] ?? s}
75 + {e.therapy_slug_names?.[j] ?? s}
76 76 </Link>
77 77 </span>
78 78 ))
@@ -114,7 +114,7 @@ export function EvidenceTable({ items, prov, showCancer = false, showVariant = t
114 114 <td className="max-w-[360px] text-[12.5px] text-ink-2">
115 115 <details>
116 116 <summary className="ci-link">EID{e.civic_id}</summary>
117 − <p className="mt-1">{e.description ?? 'No description at source.'}</p>
117 + <p className="mt-1">{e.description ? `${e.description}${e.description.length >= 320 ? '…' : ''}` : 'No description at source.'}{e.description && e.description.length >= 320 ? <span className="text-ink-3"> (full text at CIViC)</span> : null}</p>
118 118 {e.pmid ? (
119 119 <p className="mt-1">
120 120 PMID{' '}
modified apps/web/src/lib/queries/evidence.ts +9 −5
@@ -36,29 +36,33 @@ export interface EvidenceItem {
36 36 variant_slugs: string[] | null;
37 37 variant_names: string[] | null;
38 38 therapy_slugs: string[] | null;
39 + /** Drug names aligned with therapy_slugs (same ORDER BY) — never zip with therapy_names, whose order is CIViC's. */
40 + therapy_slug_names: string[] | null;
39 41 }
40 42
41 43 const SELECT = sql`
42 44 SELECT e.*, c.slug AS cancer_slug, c.canonical_name AS cancer_name,
43 45 (SELECT array_agg(v.slug ORDER BY v.slug) FROM variants v WHERE v.id = ANY(e.variant_ids)) AS variant_slugs,
44 46 (SELECT array_agg(coalesce(v.gene_symbol || ' ', '') || v.name ORDER BY v.slug) FROM variants v WHERE v.id = ANY(e.variant_ids)) AS variant_names,
45 − (SELECT array_agg(d.slug ORDER BY d.slug) FROM drugs d WHERE d.id = ANY(e.therapy_ids)) AS therapy_slugs
47 + (SELECT array_agg(d.slug ORDER BY d.slug) FROM drugs d WHERE d.id = ANY(e.therapy_ids)) AS therapy_slugs,
48 + (SELECT array_agg(d.name ORDER BY d.slug) FROM drugs d WHERE d.id = ANY(e.therapy_ids)) AS therapy_slug_names,
49 + left(e.description, 320) AS description
46 50 FROM civic_evidence_items e LEFT JOIN cancers c ON c.id = e.cancer_id`;
47 51
48 −export async function evidenceForCancer(cancerIds: string[], limit = 1000): Promise<EvidenceItem[]> {
52 +export async function evidenceForCancer(cancerIds: string[], limit = 300): Promise<EvidenceItem[]> {
49 53 if (cancerIds.length === 0) return [];
50 54 return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE e.cancer_id IN (${sql.join(cancerIds.map((i) => sql`${i}`), sql`, `)}) ORDER BY e.evidence_level NULLS LAST, e.evidence_rating DESC NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]);
51 55 }
52 56
53 −export async function evidenceForVariant(variantId: string, limit = 500): Promise<EvidenceItem[]> {
57 +export async function evidenceForVariant(variantId: string, limit = 200): Promise<EvidenceItem[]> {
54 58 return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE ${variantId} = ANY(e.variant_ids) ORDER BY c.canonical_name NULLS LAST, e.evidence_level NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]);
55 59 }
56 60
57 −export async function evidenceForGene(geneId: string, symbol: string, limit = 500): Promise<EvidenceItem[]> {
61 +export async function evidenceForGene(geneId: string, symbol: string, limit = 150): Promise<EvidenceItem[]> {
58 62 return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE ${geneId} = ANY(e.gene_ids) OR ${symbol} = ANY(e.gene_symbols) ORDER BY c.canonical_name NULLS LAST, e.evidence_level NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]);
59 63 }
60 64
61 −export async function evidenceForDrug(drugId: string, limit = 500): Promise<EvidenceItem[]> {
65 +export async function evidenceForDrug(drugId: string, limit = 150): Promise<EvidenceItem[]> {
62 66 return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE ${drugId} = ANY(e.therapy_ids) ORDER BY c.canonical_name NULLS LAST, e.evidence_level NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]);
63 67 }
64 68
modified apps/web/src/lib/queries/genomics.ts +1 −1
@@ -142,7 +142,7 @@ export async function getVariantBySlug(slug: string): Promise<VariantRow | null>
142 142 return rows[0] ?? null;
143 143 }
144 144
145 −export async function variantsForGene(geneId: string, limit = 300): Promise<VariantRow[]> {
145 +export async function variantsForGene(geneId: string, limit = 100): Promise<VariantRow[]> {
146 146 return safe(() => run<VariantRow>(sql`SELECT v.*, (SELECT count(*) FROM civic_evidence_items e WHERE v.id = ANY(e.variant_ids))::int AS evidence_count FROM variants v WHERE v.gene_id = ${geneId} ORDER BY evidence_count DESC, v.name LIMIT ${limit}`), [] as VariantRow[]);
147 147 }
148 148
149 149