spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1/**2 * Pure ranking primitives (CLAUDE.md §155: deterministic given fixed inputs).3 */4export 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}1112export interface RankOutput extends RankInput {13 rank: number;14 percentile: number; // 0..100, 100 = most extreme in the ranking direction15 eligible: number;16}1718/**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 entities21 * ranked at or below this entity in the ranking direction.22 */23export 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}3738/** Percentile of each entity for a metric (0..100, higher value → higher percentile). */39export 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}4344/** Stable hash of ranking inputs so a snapshot is reproducible/auditable. */45export 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}51