/** 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 }; } /** Immutable numeric Wikipedia metrics committed to a minted block. */ export type MintStatsSnapshot = { capturedAt: string; revisionId: number; pageBytes: number; languageCount: number; languagesTruncated: boolean; pageviews: PageEnrichment['pageviews']; stats: PageEnrichment['stats']; }; export function snapshotStats(data: PageEnrichment): MintStatsSnapshot { return { capturedAt: new Date().toISOString(), revisionId: data.revisionId, pageBytes: data.pageBytes, languageCount: data.languageCount, languagesTruncated: data.languagesTruncated, pageviews: { ...data.pageviews, months: data.pageviews.months.map(item => ({ ...item })) }, stats: { ...data.stats }, }; } /** 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) }; }