/** * String normalization used by reconciliation (CLAUDE.md §69). Deterministic, no LLM. */ const GREEK: Record = { α: 'alpha', β: 'beta', γ: 'gamma', δ: 'delta', κ: 'kappa', λ: 'lambda', μ: 'mu' }; export function stripDiacritics(s: string): string { return s.normalize('NFKD').replace(/[̀-ͯ]/g, ''); } /** Lowercase, ASCII, punctuation → space, collapse whitespace. Keeps digits (G12C, HER2). */ export function normalizeLabel(input: string): string { let s = stripDiacritics(input).toLowerCase(); s = s.replace(/[αβγδκλμ]/g, (c) => GREEK[c] ?? c); s = s.replace(/['’`"]/g, ''); s = s.replace(/[^a-z0-9]+/g, ' '); s = s.replace(/\b(nos|unspecified|not otherwise specified)\b/g, ' '); 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'); return s.replace(/\s+/g, ' ').trim(); } /** "Carcinoma, Non-Small-Cell Lung" (MeSH inverted form) → "Non-Small-Cell Lung Carcinoma". */ export function uninvertMeshTerm(term: string): string { const parts = term.split(',').map((p) => p.trim()); if (parts.length < 2) return term; return [...parts.slice(1), parts[0]].join(' '); } /** URL slug for entity routes. */ export function slugify(input: string): string { return stripDiacritics(input) .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 120); } export function sha256Hex(data: string | Uint8Array): Promise { return import('node:crypto').then(({ createHash }) => createHash('sha256').update(data).digest('hex')); }