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%
2.0 KB · 51 lines typescript
Raw Blame History
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