/** * CancerIndex stable public identifiers (CLAUDE.md §6). * * CI-CAN-00000001 cancer entity * CI-GENE-00000001 gene * CI-VAR-00000001 variant * CI-DRUG-00000001 drug * CI-TRIAL-00000001 clinical trial * CI-PUB-00000001 publication * CI-BIO-00000001 biomarker * CI-STUDY-00000001 genomic study / cohort * CI-METRIC-00000001 metric definition * CI-SOURCE-00000001 source * CI-ORG-00000001 organization * CI-TRT-00000001 treatment / regimen * CI-ANAT-00000001 anatomical site * CI-GEO-00000001 geography * CI-PROV-00000001 provenance record * * IDs are minted from a database sequence per namespace (see @cancerindex/database `mintId`) and * are never reused. Database auto-increment integers are never exposed publicly. */ export const ID_NAMESPACES = [ 'CAN', 'GENE', 'VAR', 'DRUG', 'TRIAL', 'PUB', 'BIO', 'STUDY', 'METRIC', 'SOURCE', 'ORG', 'TRT', 'ANAT', 'GEO', 'PROV', 'EDGE', 'RANK', ] as const; export type IdNamespace = (typeof ID_NAMESPACES)[number]; export const ID_WIDTH = 8; export function formatId(ns: IdNamespace, n: number): string { if (!Number.isInteger(n) || n < 1) throw new Error(`invalid id number ${n}`); return `CI-${ns}-${String(n).padStart(ID_WIDTH, '0')}`; } const ID_RE = /^CI-([A-Z]+)-(\d{8,})$/; export function parseId(id: string): { ns: IdNamespace; n: number } | null { const m = ID_RE.exec(id); if (!m) return null; const ns = m[1] as IdNamespace; if (!ID_NAMESPACES.includes(ns)) return null; return { ns, n: Number(m[2]) }; } export function isCiId(id: string, ns?: IdNamespace): boolean { const p = parseId(id); return !!p && (ns ? p.ns === ns : true); } /** Ingest run identifiers: ING-CLINICALTRIALS-20260908-000019 (CLAUDE.md §176). */ export function formatRunId(connectorId: string, date: Date, seq: number): string { const d = date.toISOString().slice(0, 10).replace(/-/g, ''); return `ING-${connectorId.toUpperCase().replace(/[^A-Z0-9]/g, '')}-${d}-${String(seq).padStart(6, '0')}`; }