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.
This commit is contained in:
parent
414ea65fee
commit
98d0946975
@ -14,7 +14,9 @@
|
|||||||
*
|
*
|
||||||
* Fallback (when search returns nothing):
|
* Fallback (when search returns nothing):
|
||||||
* Form-factor matching — for each port type in switch.ports_config,
|
* 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
|
* Used as spec_match rather than vendor_compat — shown in dashboard
|
||||||
* as "by form factor" rather than "explicitly verified".
|
* as "by form factor" rather than "explicitly verified".
|
||||||
*
|
*
|
||||||
@ -63,6 +65,18 @@ function portsConfigToFormFactors(portsConfig: Record<string, number>): string[]
|
|||||||
return [...factors];
|
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<string, number> | 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 ────────────────────────────────────────────────────
|
// ── Flexoptix search API ────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface FlexoptixSuggestion {
|
interface FlexoptixSuggestion {
|
||||||
@ -105,8 +119,9 @@ export async function scrapeFlexoptixCompatibility(): Promise<void> {
|
|||||||
model: string;
|
model: string;
|
||||||
vendor_name: string;
|
vendor_name: string;
|
||||||
ports_config: Record<string, number> | null;
|
ports_config: Record<string, number> | 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
|
FROM switches sw
|
||||||
JOIN vendors v ON v.id = sw.vendor_id
|
JOIN vendors v ON v.id = sw.vendor_id
|
||||||
ORDER BY sw.max_speed_gbps DESC`,
|
ORDER BY sw.max_speed_gbps DESC`,
|
||||||
@ -182,7 +197,15 @@ export async function scrapeFlexoptixCompatibility(): Promise<void> {
|
|||||||
// ── Strategy 2: Form-factor fallback ─────────────────────────────────
|
// ── Strategy 2: Form-factor fallback ─────────────────────────────────
|
||||||
if (sw.ports_config) {
|
if (sw.ports_config) {
|
||||||
const formFactors = portsConfigToFormFactors(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) {
|
for (const ff of formFactors) {
|
||||||
const { rows: txRows } = await pool.query<{ id: string }>(
|
const { rows: txRows } = await pool.query<{ id: string }>(
|
||||||
@ -190,11 +213,14 @@ export async function scrapeFlexoptixCompatibility(): Promise<void> {
|
|||||||
FROM transceivers t
|
FROM transceivers t
|
||||||
WHERE t.vendor_id = $1
|
WHERE t.vendor_id = $1
|
||||||
AND t.form_factor = $2
|
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 (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM compatibility c
|
SELECT 1 FROM compatibility c
|
||||||
WHERE c.switch_id = $3 AND c.transceiver_id = t.id
|
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) {
|
for (const tx of txRows) {
|
||||||
|
|||||||
237
sql/125-switch-quality.sql
Normal file
237
sql/125-switch-quality.sql
Normal file
@ -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');
|
||||||
Loading…
x
Reference in New Issue
Block a user