/** Live, off-chain Wikipedia data. Text fields are plain strings, never HTML. */ export interface PageEnrichment { pageId: number; title: string; description: string | null; wikidataId: string | null; revisionId: number; pageBytes: number; /** Number of returned interlanguage links (a lower bound when truncated). */ languageCount: number; languagesTruncated: boolean; categories: string[]; categoriesTruncated: boolean; fetchedAt: string; pageviews: { status: 'available' | 'unavailable'; /** Six completed UTC months, oldest first, identified as YYYY-MM. */ months: { month: string; views: number }[]; latestMonth: string; total: number | null; /** Latest completed month's views divided by its actual UTC day count. */ dailyAverage: number | null; trendPercent: number | null; }; stats: { attention: number | null; depth: number; reach: number }; } /** Attention uses the latest completed month's views, not the six-month total. */ export function deriveStats(pageBytes: number, languageCount: number, monthlyViews: number | null): PageEnrichment['stats'] { const score = (value: number, scale: number) => { if (!Number.isFinite(value) || value < 0) throw new RangeError('Invalid enrichment score input'); return Math.min(100, Math.round(Math.log10(value + 1) / scale * 100)); }; return { attention: monthlyViews === null ? null : score(monthlyViews, 6), depth: score(pageBytes, 6), reach: score(languageCount, 3) }; }