From 98d0946975c866a01a7d9406fa13e6defa8f3427 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 6 Jul 2026 06:48:16 +0000 Subject: [PATCH] fix(switch-db): eliminate physically-impossible compatibility rows + validation layer Root cause: the form-factor fallback in flexoptix-compat matched transceivers by form factor only, ignoring speed, so a 400G OSFP port matched every OSFP module incl. 1.6T (5686+ impossible rows across 266 switches). - flexoptix-compat: fallback now filters transceiver speed <= switch max port speed (derived from ports_config + stored max_speed_gbps). Prevents regeneration. - sql/125: evidence-based effective-ceiling (stored/ports/curated) views; flag 5811 impossible spec_match rows status=incompatible (curated rows never touched, understated switches never false-flagged); backfill max_speed_gbps for 327 switches from ports_config; re-derive speed for 306 speed<=0 transceivers (form-factor capped, high-confidence only). v_switch_quality_alerts is the task list; v_switch_understated_ports surfaces switches needing datasheet enrichment. --- .../scraper/src/scrapers/flexoptix-compat.ts | 34 ++- sql/125-switch-quality.sql | 237 ++++++++++++++++++ 2 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 sql/125-switch-quality.sql diff --git a/packages/scraper/src/scrapers/flexoptix-compat.ts b/packages/scraper/src/scrapers/flexoptix-compat.ts index d191905..335afc5 100644 --- a/packages/scraper/src/scrapers/flexoptix-compat.ts +++ b/packages/scraper/src/scrapers/flexoptix-compat.ts @@ -14,7 +14,9 @@ * * Fallback (when search returns nothing): * Form-factor matching — for each port type in switch.ports_config, - * find Flexoptix transceivers matching that form factor. + * find Flexoptix transceivers matching that form factor AND whose speed does + * not exceed the switch's fastest port (a transceiver faster than the port + * cannot link at rated speed — e.g. an 800G module in a 400G OSFP port). * Used as spec_match rather than vendor_compat — shown in dashboard * as "by form factor" rather than "explicitly verified". * @@ -63,6 +65,18 @@ function portsConfigToFormFactors(portsConfig: Record): string[] return [...factors]; } +// Max port speed (Gbps) implied by a ports_config, e.g. {"400G_QSFP-DD":4} → 400. +// The switch cannot host a transceiver faster than its fastest port. +function maxSpeedFromPortsConfig(portsConfig: Record | null): number { + if (!portsConfig) return 0; + let max = 0; + for (const key of Object.keys(portsConfig)) { + const m = key.match(/(\d+)G/i); + if (m) max = Math.max(max, parseInt(m[1], 10)); + } + return max; +} + // ── Flexoptix search API ──────────────────────────────────────────────────── interface FlexoptixSuggestion { @@ -105,8 +119,9 @@ export async function scrapeFlexoptixCompatibility(): Promise { model: string; vendor_name: string; ports_config: Record | null; + max_speed_gbps: number | null; }>( - `SELECT sw.id, sw.model, v.name AS vendor_name, sw.ports_config + `SELECT sw.id, sw.model, v.name AS vendor_name, sw.ports_config, sw.max_speed_gbps FROM switches sw JOIN vendors v ON v.id = sw.vendor_id ORDER BY sw.max_speed_gbps DESC`, @@ -182,7 +197,15 @@ export async function scrapeFlexoptixCompatibility(): Promise { // ── Strategy 2: Form-factor fallback ───────────────────────────────── if (sw.ports_config) { const formFactors = portsConfigToFormFactors(sw.ports_config); - console.log(` Form factors: ${formFactors.join(", ")}`); + // Physical speed ceiling: a transceiver faster than the switch's fastest + // port cannot link at its rated speed, so it is NOT compatible-by-form-factor. + // (ceiling <= 0 → the query below matches nothing, i.e. no fallback when the + // switch has no derivable port speed.) + const ceiling = Math.max( + maxSpeedFromPortsConfig(sw.ports_config), + sw.max_speed_gbps ?? 0, + ); + console.log(` Form factors: ${formFactors.join(", ")} (≤ ${ceiling}G)`); for (const ff of formFactors) { const { rows: txRows } = await pool.query<{ id: string }>( @@ -190,11 +213,14 @@ export async function scrapeFlexoptixCompatibility(): Promise { FROM transceivers t WHERE t.vendor_id = $1 AND t.form_factor = $2 + AND t.speed_gbps IS NOT NULL + AND t.speed_gbps > 0 + AND t.speed_gbps <= $4 AND NOT EXISTS ( SELECT 1 FROM compatibility c WHERE c.switch_id = $3 AND c.transceiver_id = t.id )`, - [flexoptixVendorId, ff, sw.id], + [flexoptixVendorId, ff, sw.id, ceiling], ); for (const tx of txRows) { diff --git a/sql/125-switch-quality.sql b/sql/125-switch-quality.sql new file mode 100644 index 0000000..c04a091 --- /dev/null +++ b/sql/125-switch-quality.sql @@ -0,0 +1,237 @@ +-- sql/125-switch-quality.sql +-- Switch / compatibility data-quality remediation + monitoring layer. +-- +-- Audit 2026-07-06 (read-only) found systematic errors in the switch database: +-- * 5.686+ physically-impossible compatibility rows (transceiver faster than the +-- switch's fastest port). Root cause: the form-factor fallback in +-- packages/scraper/src/scrapers/flexoptix-compat.ts matched by form factor ONLY, +-- ignoring speed -> a 400G-OSFP port matched every OSFP transceiver incl. 1.6T. +-- * 420/687 switches with NULL max_speed_gbps (ports_config present -> derivable). +-- * 528 transceivers with speed_gbps <= 0 (blocks compat validation; they silently +-- pass as "compatible" with everything). +-- * coding-pattern coverage as low as ~13% at 100G (vendor_compat = [] for most). +-- +-- DESIGN (why this is safe at the "no errors" bar): +-- The naive ceiling (ports_config-derived max) is UNRELIABLE because some switches' +-- ports_config is understated (e.g. Cisco C9500-32QC lists 40G but does 100G; NCS 540 +-- "Z" ports list 10G but are 25G-capable; A9K-20HG-FLEX lists 100G but does 400G). +-- Flagging by that ceiling would create FALSE "incompatible" flags on curated rows. +-- So the effective ceiling is evidence-based: +-- effective_max = GREATEST(stored max_speed_gbps, +-- ports_config-derived max, +-- fastest curated-compatible transceiver for the switch) +-- and ONLY algorithmic 'spec_match' fallback rows are ever auto-flagged; curated +-- ('vendor_matrix'/'vendor_compat') rows are never touched (they instead RAISE the +-- ceiling, protecting understated switches). Result: 0 curated false positives. +-- +-- Idempotent + safe to re-run. Applied with psql --single-transaction. + +-- ═══════════════════════════════════════════════════════════════════════════ +-- 0) Ports-config -> max port speed (pure helper) + evidence-based switch ceiling +-- ═══════════════════════════════════════════════════════════════════════════ + +create or replace function fn_ports_max_gbps(cfg jsonb) returns numeric +language sql immutable as $fn$ + select case when jsonb_typeof(cfg) = 'object' + then (select max(substring(k from '([0-9]+)G')::numeric) + from jsonb_object_keys(cfg) k) + else null end +$fn$; + +comment on function fn_ports_max_gbps(jsonb) is + 'Max port speed (Gbps) parsed from a switch ports_config JSONB object, e.g. {"400G_QSFP-DD":4,"100G_QSFP28":2} -> 400. NULL if config is empty/non-object.'; + +create or replace view v_switch_effective_max as +select s.id as switch_id, + s.max_speed_gbps as stored_max_gbps, + fn_ports_max_gbps(s.ports_config) as ports_max_gbps, + (select max(t.speed_gbps) + from compatibility c join transceivers t on t.id = c.transceiver_id + where c.switch_id = s.id and c.status = 'compatible' + and c.verification_method in ('vendor_matrix','vendor_compat') + and t.speed_gbps > 0) as curated_max_gbps, + greatest( + coalesce(s.max_speed_gbps, 0), + coalesce(fn_ports_max_gbps(s.ports_config), 0), + coalesce((select max(t.speed_gbps) + from compatibility c join transceivers t on t.id = c.transceiver_id + where c.switch_id = s.id and c.status = 'compatible' + and c.verification_method in ('vendor_matrix','vendor_compat') + and t.speed_gbps > 0), 0) + ) as effective_max_gbps +from switches s; + +comment on view v_switch_effective_max is + 'Evidence-based max port speed per switch (greatest of stored max_speed_gbps, ports_config-derived, and fastest curated-compatible transceiver). Used to validate compatibility without false-flagging switches whose ports_config is understated.'; + +-- ═══════════════════════════════════════════════════════════════════════════ +-- REMEDIATION A) Backfill max_speed_gbps from ports_config (additive, nulls only). +-- Highest leverage: unblocks validation for switches that currently pass no check. +-- Only fills NULLs; never overwrites a curated stored value (61/267 stored values +-- disagree with ports-derived in both directions -> curated values are preserved). +-- Switches with an empty {} ports_config stay NULL (need a datasheet -> see alerts). +-- ═══════════════════════════════════════════════════════════════════════════ + +update switches s + set max_speed_gbps = fn_ports_max_gbps(s.ports_config) + where s.max_speed_gbps is null + and fn_ports_max_gbps(s.ports_config) is not null + and fn_ports_max_gbps(s.ports_config) > 0; + +-- ═══════════════════════════════════════════════════════════════════════════ +-- REMEDIATION C) Re-derive speed_gbps for transceivers with speed <= 0, HIGH +-- confidence only. Speed comes from the manufacturer's standardized speed token +-- in the part number (e.g. SFP-10G-SR -> 10, QDD-2X400G -> 800, QSFP-4SFP25G -> 100), +-- and is applied ONLY when it is <= the form factor's physical maximum. Parts whose +-- token exceeds the form-factor cap (usually a wrong form_factor, e.g. QSFP-400G-AOC +-- mislabelled 'SFP') or that have no speed token are LEFT flagged for datasheet work. +-- No blind update: FastEthernet "100FX"/"100BASE" (no 'G') never matches. +-- ═══════════════════════════════════════════════════════════════════════════ + +with cand as ( + select id, + case + when part_number ~* '([0-9]+)X([0-9]+)G' + then (regexp_match(part_number,'([0-9]+)X([0-9]+)G','i'))[1]::numeric + * (regexp_match(part_number,'([0-9]+)X([0-9]+)G','i'))[2]::numeric + when part_number ~* '([0-9]+)SFP([0-9]+)G' + then (regexp_match(part_number,'([0-9]+)SFP([0-9]+)G','i'))[1]::numeric + * (regexp_match(part_number,'([0-9]+)SFP([0-9]+)G','i'))[2]::numeric + when part_number ~* '([0-9]+)G' + then (regexp_match(part_number,'([0-9]+)G','i'))[1]::numeric + end as derived, + case upper(coalesce(form_factor,'')) + when 'SFP' then 1 when 'SFP+' then 16 when 'SFP28' then 32 when 'SFP56' then 56 + when 'XFP' then 10 when 'QSFP+' then 40 when 'QSFP28' then 100 when 'QSFP56' then 200 + when 'QSFP-DD' then 800 when 'QSFP-DD800' then 800 + when 'OSFP' then 1600 when 'OSFP224' then 1600 + when 'CFP' then 100 when 'CFP2' then 400 when 'CFP4' then 100 when 'CPAK' then 100 + else 99999 end as ff_cap + from transceivers + where speed_gbps is null or speed_gbps <= 0) +update transceivers t + set speed_gbps = c.derived + from cand c + where t.id = c.id + and c.derived is not null and c.derived > 0 + and c.derived <= c.ff_cap; + +-- ═══════════════════════════════════════════════════════════════════════════ +-- REMEDIATION B) Flag physically-impossible compatibility rows. +-- Only 'spec_match' (algorithmic form-factor fallback) rows are flagged; curated +-- rows are never touched. status='incompatible' (constraint-allowed value) removes +-- them from every consumer that filters status='compatible' (e.g. queries.ts:302). +-- Runs AFTER A + C so it uses backfilled ceilings and corrected transceiver speeds. +-- ═══════════════════════════════════════════════════════════════════════════ + +with em as materialized ( + select switch_id, effective_max_gbps from v_switch_effective_max) +update compatibility c + set status = 'incompatible', + notes = case when c.notes is null or c.notes = '' + then 'auto(125): transceiver speed exceeds switch max port speed' + else c.notes || ' | auto(125): transceiver speed exceeds switch max port speed' end + from em e, transceivers t + where e.switch_id = c.switch_id + and t.id = c.transceiver_id + and c.status = 'compatible' + and c.verification_method = 'spec_match' + and e.effective_max_gbps > 0 + and t.speed_gbps is not null + and t.speed_gbps > e.effective_max_gbps; + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MONITORING VIEWS (read-only). v_switch_quality_alerts is the task list. +-- ═══════════════════════════════════════════════════════════════════════════ + +-- 1) Impossible rows still CLAIMING compatibility (drains to 0 after remediation B; +-- lights up again if a future scrape inserts a new impossible spec_match row). +create or replace view v_compat_speed_errors as +select c.id as compat_id, + s.model as switch, + e.effective_max_gbps as switch_max_gbps, + t.part_number as transceiver, + t.speed_gbps as transceiver_gbps, + round(t.speed_gbps / nullif(e.effective_max_gbps,0), 1) as over_ratio +from compatibility c +join v_switch_effective_max e on e.switch_id = c.switch_id +join switches s on s.id = c.switch_id +join transceivers t on t.id = c.transceiver_id +where c.status = 'compatible' + and c.verification_method = 'spec_match' + and e.effective_max_gbps > 0 + and t.speed_gbps is not null + and t.speed_gbps > e.effective_max_gbps; + +-- 2) Understated switch port data: a CURATED source pairs a transceiver faster than the +-- switch's stored/ports max. These are NOT errors to auto-fix -- they flag switches +-- whose ports_config needs datasheet enrichment (or a wrong curated claim to review). +create or replace view v_switch_understated_ports as +select c.id as compat_id, + v.name as manufacturer, + s.model as switch, + s.max_speed_gbps as stored_max_gbps, + fn_ports_max_gbps(s.ports_config) as ports_max_gbps, + t.part_number as transceiver, + t.speed_gbps as transceiver_gbps, + c.verification_method +from compatibility c +join switches s on s.id = c.switch_id +join vendors v on v.id = s.vendor_id +join transceivers t on t.id = c.transceiver_id +where c.status = 'compatible' + and c.verification_method in ('vendor_matrix','vendor_compat') + and t.speed_gbps is not null + and (coalesce(s.max_speed_gbps,0) > 0 or coalesce(fn_ports_max_gbps(s.ports_config),0) > 0) + and t.speed_gbps > greatest(coalesce(s.max_speed_gbps,0), coalesce(fn_ports_max_gbps(s.ports_config),0)); + +-- 3) Coding-pattern coverage per speed tier (module only) -- data-SOURCE gap, pipeline task. +create or replace view v_coding_coverage as +select t.speed_gbps::numeric as tier, + count(*)::int as modules, + count(*) filter (where jsonb_array_length(coalesce(t.vendor_compat,'[]'::jsonb)) > 0)::int as with_coding, + round(100.0 * count(*) filter (where jsonb_array_length(coalesce(t.vendor_compat,'[]'::jsonb)) > 0) + / count(*), 1) as coverage_pct +from transceivers t +where t.product_type = 'module' and t.speed_gbps >= 100 +group by 1 order by 1; + +-- 4) Transceivers with an impossible speed (<=0). 100 Mbps (0.1) and FC/SONET rates +-- (0.155/0.622/2.488/8/8.5/14/28/32) are legitimate and intentionally NOT flagged. +create or replace view v_transceiver_speed_errors as +select id, part_number, speed_gbps, form_factor +from transceivers +where speed_gbps is null or speed_gbps <= 0; + +-- 5) Switches that cannot validate compatibility (missing max_speed / total_ports). +create or replace view v_switch_completeness as +select s.id, v.name as manufacturer, s.model, s.max_speed_gbps, s.total_ports, + (jsonb_typeof(s.ports_config) = 'object' and s.ports_config <> '{}'::jsonb) as has_ports_config +from switches s join vendors v on v.id = s.vendor_id +where s.max_speed_gbps is null or s.total_ports is null; + +-- 6) One-line alerts = the switch/coding data-quality task list. +create or replace view v_switch_quality_alerts as +select 'compatibility: ' || count(*) || ' physically-impossible spec_match rows (transceiver faster than switch)' as alert, + 'critical' as severity from v_compat_speed_errors +union all +select 'coding matrix: ' || tier || 'G only ' || coverage_pct || '% populated (' || (modules - with_coding) || ' modules without coding patterns)', + case when coverage_pct < 25 then 'high' else 'warning' end +from v_coding_coverage where coverage_pct < 70 +union all +select 'transceivers: ' || count(*) || ' with speed_gbps <= 0 (need form_factor/datasheet)', 'high' +from v_transceiver_speed_errors +union all +select 'switches: ' || count(*) || ' missing max_speed_gbps (empty ports_config -> needs datasheet)', 'warning' +from v_switch_completeness where max_speed_gbps is null +union all +select 'switches: ' || count(*) || ' with understated port data (curated compat faster than ports_config)', 'warning' +from v_switch_understated_ports; + +-- Usage: select * from v_switch_quality_alerts order by severity; +-- select * from v_compat_speed_errors order by over_ratio desc limit 50; +-- select * from v_switch_understated_ports order by transceiver_gbps desc; + +insert into _migrations (filename, applied_at) +select '125-switch-quality.sql', now() +where not exists (select 1 from _migrations where filename = '125-switch-quality.sql');