/* Maintains packages/share/src/counter-utils.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries. Do not depend on GSAP or DOM. Do not handle animation logic. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ export interface CounterStat { value?: string; label: string; numericValue?: number; prefix?: string; suffix?: string; decimals?: number; duration?: number; } export interface ResolvedCounterStat extends CounterStat { isAnimated: boolean; startValue: string; finalFormatted: string; } export function formatNumber(value: number, locale: string, decimals = 0): string { return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(value); } export function parseNumeric(val: string | undefined): number | undefined { if (!val) return undefined; const cleaned = val.replace(/[^\d.,]/g, "").replace(/,/g, ""); const num = parseFloat(cleaned); return isNaN(num) ? undefined : num; } // Create start value with same width as final, starting from "1" in highest digit export function getStartValue(formattedFinal: string): string { // Replace digits: first digit becomes "1", rest become "0" // "1,250" → "1,000", "14" → "10" let firstDigit = true; return formattedFinal.replace(/\d/g, () => { if (firstDigit) { firstDigit = false; return "1"; } return "0"; }); } export function resolveCounterStats( stats: CounterStat[], animated: boolean | undefined, locale: string, ): ResolvedCounterStat[] { return stats.map((stat) => { const numericValue = stat.numericValue ?? parseNumeric(stat.value); const isAnimated: boolean = (animated ?? false) && numericValue !== undefined; const decimals = stat.decimals ?? 0; const finalFormatted = numericValue !== undefined ? formatNumber(numericValue, locale, decimals) : (stat.value ?? ""); const startValue = isAnimated ? getStartValue(finalFormatted) : finalFormatted; return { ...stat, numericValue, isAnimated, startValue, finalFormatted }; }); }