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.
This commit is contained in:
Rene Fichtmueller 2026-06-11 05:19:48 +00:00
parent 2b7d5c7037
commit 436f62bc2b

View File

@ -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
]);
}