/** * Hype Cycle Data Enrichment — Real metrics from scraped data * * Computes PhaseMetrics overrides from actual database observations: * - vendorCount: How many vendors sell this speed class * - price trends: ASP decline rate from price_observations * - catalog density: Number of SKUs per speed class (market maturity signal) * - product diversity: Form factor and reach variety */ import { pool } from "../db/client"; import type { PhaseMetrics } from "./norton-bass"; interface SpeedClassMetrics { speedGbps: number; vendorCount: number; skuCount: number; avgPrice?: number; minPrice?: number; maxPrice?: number; priceCount: number; formFactors: string[]; reachVariants: number; } /** * Query real vendor/product counts per speed class from the database. */ export async function getSpeedClassMetrics(): Promise> { const result = await pool.query(` SELECT t.speed_gbps, COUNT(DISTINCT t.vendor_id) AS vendor_count, COUNT(DISTINCT t.id) AS sku_count, ARRAY_AGG(DISTINCT t.form_factor) FILTER (WHERE t.form_factor IS NOT NULL AND t.form_factor != '') AS form_factors, COUNT(DISTINCT t.reach_label) FILTER (WHERE t.reach_label IS NOT NULL AND t.reach_label != '') AS reach_variants FROM transceivers t WHERE t.speed_gbps > 0 GROUP BY t.speed_gbps ORDER BY t.speed_gbps `); const priceResult = await pool.query(` SELECT t.speed_gbps, AVG(po.price) AS avg_price, MIN(po.price) AS min_price, MAX(po.price) AS max_price, COUNT(*) AS price_count FROM price_observations po JOIN transceivers t ON t.id = po.transceiver_id WHERE t.speed_gbps > 0 GROUP BY t.speed_gbps `); const priceMap = new Map(); for (const row of priceResult.rows) { priceMap.set(Number(row.speed_gbps), { avg: parseFloat(row.avg_price), min: parseFloat(row.min_price), max: parseFloat(row.max_price), count: parseInt(row.price_count), }); } return result.rows.map((row) => { const speedGbps = Number(row.speed_gbps); const priceData = priceMap.get(speedGbps); return { speedGbps, vendorCount: parseInt(row.vendor_count), skuCount: parseInt(row.sku_count), avgPrice: priceData?.avg, minPrice: priceData?.min, maxPrice: priceData?.max, priceCount: priceData?.count ?? 0, formFactors: row.form_factors || [], reachVariants: parseInt(row.reach_variants), }; }); } /** * Convert raw speed-class metrics into PhaseMetrics overrides. * These override the model-estimated values with real data. */ export function metricsToPhaseOverrides( metrics: SpeedClassMetrics, totalMarketSkus: number, ): Partial { const overrides: Partial = {}; // Vendor count — direct from data overrides.vendorCount = metrics.vendorCount; // Vendor trend — estimate from catalog density // More SKUs + more vendors = increasing; few = decreasing if (metrics.vendorCount >= 4 && metrics.skuCount > 50) { overrides.vendorTrend = "stable"; } else if (metrics.vendorCount >= 2) { overrides.vendorTrend = "increasing"; } else { overrides.vendorTrend = "decreasing"; } // Shipment share proxy — SKU count relative to total market overrides.shipmentShare = Math.min(0.5, metrics.skuCount / Math.max(1, totalMarketSkus)); // Interop level — more reach variants and form factors = better interop const ffDiversity = metrics.formFactors.length; const reachDiversity = metrics.reachVariants; overrides.interopLevel = Math.min(100, ffDiversity * 15 + reachDiversity * 8); return overrides; } /** * Get enriched PhaseMetrics for all speed classes. * Returns a map of speedGbps -> partial PhaseMetrics overrides. */ export async function getDataDrivenOverrides(): Promise>> { const allMetrics = await getSpeedClassMetrics(); const totalSkus = allMetrics.reduce((sum, m) => sum + m.skuCount, 0); const overridesMap = new Map>(); for (const metrics of allMetrics) { overridesMap.set(metrics.speedGbps, metricsToPhaseOverrides(metrics, totalSkus)); } return overridesMap; } /** * Revenue lifecycle prediction per speed class. * * Uses scraped price data + Bass diffusion to estimate: * - Peak revenue year * - Revenue duration (years above 50% of peak) * - Current revenue trajectory */ export interface RevenueLifecycle { speedGbps: number; technology: string; currentAvgPrice?: number; estimatedPeakRevenueYear: number; estimatedDeclineStartYear: number; revenueHalfLifeYears: number; currentPhase: "growing" | "peaking" | "declining" | "legacy"; revenueIndex: number; // 0-100, relative to estimated peak } export function computeRevenueLifecycle( speedGbps: number, techName: string, introYear: number, peakYear: number, currentYear: number, avgPrice?: number, ): RevenueLifecycle { // Revenue = Price × Volume. Price declines while volume grows. // Peak revenue happens ~2 years before peak volume (when price×volume is maximized) const peakRevenueYear = Math.round(peakYear - 2); const declineStartYear = peakYear + 2; const halfLife = Math.round((peakYear - introYear) * 0.7); const yearsFromPeak = currentYear - peakRevenueYear; let currentPhase: RevenueLifecycle["currentPhase"]; if (currentYear < peakRevenueYear - 2) currentPhase = "growing"; else if (currentYear <= peakRevenueYear + 2) currentPhase = "peaking"; else if (currentYear <= declineStartYear + halfLife) currentPhase = "declining"; else currentPhase = "legacy"; // Revenue index: bell curve centered on peakRevenueYear const sigma = halfLife / 1.5; const revenueIndex = Math.round(100 * Math.exp(-0.5 * Math.pow(yearsFromPeak / sigma, 2))); return { speedGbps, technology: techName, currentAvgPrice: avgPrice, estimatedPeakRevenueYear: peakRevenueYear, estimatedDeclineStartYear: declineStartYear, revenueHalfLifeYears: halfLife, currentPhase, revenueIndex, }; } /** * Regional adoption model. * Applies lag coefficients per region based on industry research. */ export interface RegionalAdoption { region: string; lagYears: number; marketSharePct: number; adoptionPhase: string; estimatedPeakYear: number; } /** * Regional lag coefficients calibrated from research (2026-03-28). * Sources: LightCounting, vendor earnings, OFC market sessions, Chinese IPO prospectuses. * Lag in years (converted from quarters: NA=0, CN=0.5Q≈0.125yr, EU=4Q=1yr, etc.) */ const REGIONAL_LAGS: ReadonlyArray<{ region: string; lagYears: number; marketSharePct: number; priceIndex: number; segmentMix: { hyperscaler: number; telco: number; enterprise: number }; }> = [ { region: "North America (Hyperscale)", lagYears: 0, marketSharePct: 32, priceIndex: 1.0, segmentMix: { hyperscaler: 0.65, telco: 0.20, enterprise: 0.15 }, }, { region: "China (BAT/Hyperscale)", lagYears: 0.125, // 0.5 quarters = near-parity, sometimes leads on volume marketSharePct: 38, priceIndex: 0.58, // 42% cheaper domestically segmentMix: { hyperscaler: 0.50, telco: 0.35, enterprise: 0.15 }, }, { region: "APAC (ex-China)", lagYears: 0.625, // 2.5 quarters marketSharePct: 13, priceIndex: 1.0, segmentMix: { hyperscaler: 0.35, telco: 0.40, enterprise: 0.25 }, }, { region: "Europe", lagYears: 1.0, // 4 quarters — telco procurement cycles, regulatory compliance marketSharePct: 14, priceIndex: 1.12, // 12% premium (CE/RoHS compliance, smaller volumes, channel markup) segmentMix: { hyperscaler: 0.25, telco: 0.45, enterprise: 0.30 }, }, { region: "Rest of World", lagYears: 1.5, // 6 quarters marketSharePct: 3, priceIndex: 1.05, segmentMix: { hyperscaler: 0.20, telco: 0.50, enterprise: 0.30 }, }, ]; export function computeRegionalAdoption( techPeakYear: number, currentYear: number, techName: string, ): ReadonlyArray { return REGIONAL_LAGS.map(({ region, lagYears, marketSharePct }) => { const regionalPeak = techPeakYear + lagYears; const yearsToPeak = regionalPeak - currentYear; let adoptionPhase: string; if (yearsToPeak > 5) adoptionPhase = "Pre-adoption"; else if (yearsToPeak > 2) adoptionPhase = "Early Adoption"; else if (yearsToPeak > -1) adoptionPhase = "Growth"; else if (yearsToPeak > -4) adoptionPhase = "Mature"; else adoptionPhase = "Declining"; return { region, lagYears, marketSharePct, adoptionPhase, estimatedPeakYear: Math.round(regionalPeak * 2) / 2, // Round to half-year }; }); }