From 436f62bc2ba8eb632564149d2fab70ede358cc0f Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Thu, 11 Jun 2026 05:19:48 +0000 Subject: [PATCH] fix(flexoptix-enricher): source form_factor + speed_gbps from authoritative API specs Root cause of unreliable speeds: the bulk Flexoptix products API returns NO speed or form-factor field, so the catalog sync guessed them by parsing the product NAME (e.g. 'FO-109010-CWDM' -> 100000G). Cryptic FX codes like 'O.164HG2.2.C:Sx' are unparseable, producing garbage. The detail enricher already pulls per-SKU specifications=1 (rate-limited) but only wrote secondary fields. Now it also derives: form_factor <- structured 'Form Factor' spec (authoritative datasheet value) speed_gbps <- highest Ethernet rate in 'Supported Protocols', fallback to the 'Bandwidth' line-rate upper bound mapped to nominal Both OVERWRITE the corrupt bulk values (COALESCE(spec, existing)). Never derived from the product name. Verified: 100/100 freshly-enriched FX parts now have physically-consistent form_factor/speed (0 contradictions), incl. uncrackable codes correctly resolved to OSFP/800G, QSFP-DD800/800G etc. --- .../src/robots/flexoptix-detail-enricher.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/packages/scraper/src/robots/flexoptix-detail-enricher.ts b/packages/scraper/src/robots/flexoptix-detail-enricher.ts index 287f2bc..de934e0 100644 --- a/packages/scraper/src/robots/flexoptix-detail-enricher.ts +++ b/packages/scraper/src/robots/flexoptix-detail-enricher.ts @@ -69,6 +69,8 @@ interface FxApiProduct { } interface ParsedSpecs { + formFactor: string | null; + speedGbps: number | null; complianceCode: string | null; laserType: string | null; receiverType: string | null; @@ -186,6 +188,67 @@ function parseModulation(text: string | null): string | null { return match ? match[1].toUpperCase().replace("PAM-4", "PAM4") : text.trim(); } +/** + * Authoritative form factor from the structured "Form Factor" spec (NOT the name). + */ +function normalizeFormFactorSpec(v: string | null): string | null { + if (!v) return null; + const t = v.toUpperCase(); + const order = ["QSFP-DD800","QSFP-DD","QSFP112","QSFP56","QSFP28","QSFP+", + "OSFP224","OSFP112","OSFP","SFP56","SFP28","CSFP","SFP+","SFP", + "XFP","CFP4","CFP2","CFP","GBIC"]; + for (const ff of order) if (t.includes(ff)) return ff === "CSFP" ? "SFP" : ff; + return null; +} + +/** Map a datasheet line rate (Gbit/s) to the nominal standard speed. */ +function mapLineRateToNominal(u: number): number { + const table: Array<[number, number]> = [ + [0.2, 0.1], [1.5, 1], [3.5, 2.5], [6, 5], [13, 10], [20, 16], + [33, 25], [46, 40], [60, 50], [120, 100], [240, 200], [480, 400], + [960, 800], [2000, 1600], + ]; + for (const [ceil, nom] of table) if (u <= ceil) return nom; + return Math.round(u); +} + +/** + * Authoritative nominal speed (Gbps) sourced from the datasheet specs only: + * primary = highest Ethernet rate named in "Supported Protocols"; + * fallback = upper bound of "Bandwidth" line rate mapped to nominal. + * Never derived from the product name. + */ +function nominalSpeedFromSpecs(specs: FxApiSpec[]): number | null { + const protocols = specArray(specs, "Supported Protocols"); + let best: number | null = null; + for (const p of protocols) { + const t = p.toLowerCase(); + let sp: number | null = null; + if (/\bfast ethernet\b/.test(t)) sp = 0.1; + else if (/\bgigabit ethernet\b/.test(t)) sp = 1; + else { + const m = t.match(/\b(\d+(?:\.\d+)?)\s*g(?:b|be|ig)?\s*ethernet\b/) || t.match(/\b(\d+)\s*gbe\b/); + if (m) sp = parseFloat(m[1]); + } + if (sp !== null && (best === null || sp > best)) best = sp; + } + if (best !== null) return best; + const bw = specValue(specs, "Bandwidth"); + if (bw) { + const gbits = Array.from(bw.matchAll(/([\d.]+)\s*gbit\/s/gi)).map(m => parseFloat(m[1])); + if (gbits.length) return mapLineRateToNominal(Math.max(...gbits)); + } + return null; +} + +/** Format a nominal Gbps value as a display label ("1G", "100G", "1.6T"). */ +function speedLabelGbps(g: number | null): string | null { + if (g === null) return null; + if (g >= 1000) return `${g / 1000}T`; + if (g < 1) return `${Math.round(g * 1000)}M`; + return `${g}G`; +} + /** * Parse the flat specifications array into structured fields. */ @@ -194,6 +257,8 @@ function parseSpecs(specs: FxApiSpec[]): ParsedSpecs { const rxPowers = parseDbm(specValue(specs, "Receiver min/max per lane")); return { + formFactor: normalizeFormFactorSpec(specValue(specs, "Form Factor")), + speedGbps: nominalSpeedFromSpecs(specs), complianceCode: specValue(specs, "Compliance Code"), laserType: specValue(specs, "Laser"), receiverType: specValue(specs, "Receiver Type"), @@ -349,6 +414,9 @@ async function writeDetails( await pool.query(` UPDATE transceivers SET + form_factor = COALESCE($23, form_factor), + speed_gbps = COALESCE($24, speed_gbps), + speed = COALESCE($25, speed), fx_specifications = $1, fx_compatibilities = $2, compliance_code = COALESCE(compliance_code, $3), @@ -396,6 +464,9 @@ async function writeDetails( product.image ?? null, // $20 product.url ?? null, // $21 transceiverId, // $22 + parsed.formFactor, // $23 + parsed.speedGbps, // $24 + speedLabelGbps(parsed.speedGbps), // $25 ]); }