diff --git a/sql/131-wavelength-column-sync.sql b/sql/131-wavelength-column-sync.sql new file mode 100644 index 0000000..19c8d6f --- /dev/null +++ b/sql/131-wavelength-column-sync.sql @@ -0,0 +1,60 @@ +-- sql/131 — Wavelength column sync (root-cause fix for equivalence review backlog) +-- +-- Root cause: the enricher populates the structured integer column +-- transceivers.wavelength_tx_nm, but the equivalence spec-matcher reads the TEXT +-- column transceivers.wavelengths via tip_extract_wavelength_nm(). 27,908 rows had +-- wavelength_tx_nm set while wavelengths was NULL, so the matcher could not confirm +-- wavelength on either side and dumped ~3,155 genuine equivalences into manual review. +-- +-- Fix (no fabrication — copies existing structured data into the format the matcher +-- expects, and keeps it in sync going forward): +-- A) one-off backfill of transceivers.wavelengths from wavelength_tx_nm +-- B) durable BEFORE-trigger so future ingests stay in sync +-- C) promote pending -> auto_approved ONLY where the FULL spec-matcher criteria now +-- agree (form_factor + speed + fiber_type + reach-tier + wavelength +/-10nm). +-- Everything with a genuine reach-tier / fiber / wavelength difference stays pending. + +BEGIN; + +-- A) one-off backfill (existing structured data -> text column the matcher reads) +UPDATE transceivers + SET wavelengths = wavelength_tx_nm || 'nm' + WHERE (wavelengths IS NULL OR wavelengths = '') + AND wavelength_tx_nm IS NOT NULL; + +-- B) durable sync trigger for future inserts/updates +CREATE OR REPLACE FUNCTION tip_sync_wavelengths_from_tx() RETURNS trigger AS $fn$ +BEGIN + IF (NEW.wavelengths IS NULL OR NEW.wavelengths = '') + AND NEW.wavelength_tx_nm IS NOT NULL THEN + NEW.wavelengths := NEW.wavelength_tx_nm || 'nm'; + END IF; + RETURN NEW; +END; +$fn$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_sync_wavelengths ON transceivers; +CREATE TRIGGER trg_sync_wavelengths + BEFORE INSERT OR UPDATE ON transceivers + FOR EACH ROW EXECUTE FUNCTION tip_sync_wavelengths_from_tx(); + +-- C) valid promotion: pending -> auto_approved where full spec now confirmed +UPDATE transceiver_equivalences e + SET status = 'auto_approved', + confidence = GREATEST(e.confidence, 0.85), + match_notes = COALESCE(e.match_notes,'') || ' | sql/131: full spec confirmed after wavelength column sync', + updated_at = now() + FROM transceivers fx, transceivers cx + WHERE e.flexoptix_id = fx.id + AND e.competitor_id = cx.id + AND e.status = 'pending' + AND fx.form_factor = cx.form_factor + AND fx.speed_gbps = cx.speed_gbps + AND fx.fiber_type IS NOT DISTINCT FROM cx.fiber_type + AND fx.reach_meters >= 10 AND cx.reach_meters >= 10 + AND tip_reach_tier(fx.reach_meters) = tip_reach_tier(cx.reach_meters) + AND tip_extract_wavelength_nm(fx.wavelengths) IS NOT NULL + AND tip_extract_wavelength_nm(cx.wavelengths) IS NOT NULL + AND abs(tip_extract_wavelength_nm(fx.wavelengths) - tip_extract_wavelength_nm(cx.wavelengths)) <= 10; + +COMMIT; diff --git a/sql/132-wavelength-authority-normalize.sql b/sql/132-wavelength-authority-normalize.sql new file mode 100644 index 0000000..2665bda --- /dev/null +++ b/sql/132-wavelength-authority-normalize.sql @@ -0,0 +1,74 @@ +-- sql/132 — Wavelength authority correction + format normalization +-- +-- Follow-up to sql/131. Discovery: transceivers.wavelengths holds the part-derived +-- wavelength as a BARE number ("1470") for ~16.8k rows, which tip_extract_wavelength_nm() +-- cannot read (it requires the "nm" suffix). Meanwhile wavelength_tx_nm disagrees with +-- that authoritative bare number 33% of the time, and 64% of those disagreements are a +-- systematic tx_nm=1310 enricher default. So the bare-number column is authoritative; +-- tx_nm is not. This migration prefers the authoritative source. +-- +-- 1) normalize authoritative bare-number wavelengths -> "NNNnm" (format only, no value change) +-- 2) clean "N/A" text -> NULL +-- 3) correct the few CWDM rows sql/131 backfilled with a wrong tx_nm default, using the +-- deterministic CWDM wavelength in the part number +-- 4) re-promote pending -> auto_approved where the FULL spec gate now agrees on +-- authoritative data (gate already protects against tx_nm noise: both sides must +-- agree within +/-10nm, so a wrong 1310 default can never auto-approve a real 1470). + +BEGIN; + +-- 1) normalize authoritative bare-number wavelengths (preserve value, add unit) +UPDATE transceivers + SET wavelengths = wavelengths || 'nm' + WHERE wavelengths ~ '^\d{3,4}$'; + +-- 2) non-values -> NULL (N/A, -, empty, etc.) +UPDATE transceivers + SET wavelengths = NULL + WHERE wavelengths IS NOT NULL + AND tip_extract_wavelength_nm(wavelengths) IS NULL + AND wavelengths !~ '\d'; + +-- 3) correct CWDM rows that sql/131 filled from a wrong tx_nm default: +-- use the deterministic CWDM wavelength encoded in the part number +UPDATE transceivers + SET wavelengths = (regexp_match(part_number,'(12[6-9]0|13[0-9]0|14[0-9]0|15[0-9]0|16[01]0)'))[1] || 'nm' + WHERE wavelengths = wavelength_tx_nm || 'nm' + AND part_number ~ '(12[6-9]|13[0-9]|14[0-9]|15[0-9]|16[01])0' + AND (regexp_match(part_number,'(12[6-9]0|13[0-9]0|14[0-9]0|15[0-9]0|16[01]0)'))[1]::int <> wavelength_tx_nm; + +-- 3b) upgrade the sync trigger so future ingests self-normalize: +-- prefer an authoritative bare-number wavelength (add "nm"), fall back to tx_nm only +-- when no textual wavelength is present at all. +CREATE OR REPLACE FUNCTION tip_sync_wavelengths_from_tx() RETURNS trigger AS $fn$ +BEGIN + IF NEW.wavelengths ~ '^\d{3,4}$' THEN + NEW.wavelengths := NEW.wavelengths || 'nm'; + ELSIF (NEW.wavelengths IS NULL OR NEW.wavelengths = '' OR tip_extract_wavelength_nm(NEW.wavelengths) IS NULL) + AND NEW.wavelength_tx_nm IS NOT NULL THEN + NEW.wavelengths := NEW.wavelength_tx_nm || 'nm'; + END IF; + RETURN NEW; +END; +$fn$ LANGUAGE plpgsql; + +-- 4) valid promotion on corrected/authoritative data (same full spec gate as spec-matcher) +UPDATE transceiver_equivalences e + SET status = 'auto_approved', + confidence = GREATEST(e.confidence, 0.85), + match_notes = COALESCE(e.match_notes,'') || ' | sql/132: full spec confirmed on authoritative wavelength', + updated_at = now() + FROM transceivers fx, transceivers cx + WHERE e.flexoptix_id = fx.id + AND e.competitor_id = cx.id + AND e.status = 'pending' + AND fx.form_factor = cx.form_factor + AND fx.speed_gbps = cx.speed_gbps + AND fx.fiber_type IS NOT DISTINCT FROM cx.fiber_type + AND fx.reach_meters >= 10 AND cx.reach_meters >= 10 + AND tip_reach_tier(fx.reach_meters) = tip_reach_tier(cx.reach_meters) + AND tip_extract_wavelength_nm(fx.wavelengths) IS NOT NULL + AND tip_extract_wavelength_nm(cx.wavelengths) IS NOT NULL + AND abs(tip_extract_wavelength_nm(fx.wavelengths) - tip_extract_wavelength_nm(cx.wavelengths)) <= 10; + +COMMIT; diff --git a/sql/133-reach-substitution-policy.sql b/sql/133-reach-substitution-policy.sql new file mode 100644 index 0000000..71ab77c --- /dev/null +++ b/sql/133-reach-substitution-policy.sql @@ -0,0 +1,46 @@ +-- sql/133 — One-directional reach-tier substitution policy +-- +-- Decision (Rene, 2026-07-07): a higher-reach optic may satisfy a lower-reach +-- requirement (e.g. a 40km ER can serve where a 10km LR is asked for), never the +-- reverse — the shorter-reach part cannot close the link budget on a longer run. +-- ZR/coherent/amplified tiers are explicitly EXCLUDED from this automatic rule: +-- those can overload a short-haul receiver without an attenuator, which is a real +-- power-budget calculation, not a simple tier comparison. Those stay in manual review. +-- +-- Result is tagged as its own status (compatible_superset), never merged into +-- auto_approved/exact-spec matches, so downstream consumers (Fossy/Beo/Pulso, pricing) +-- can distinguish "identical spec" from "safe higher-spec substitute" — they usually +-- differ in price and are not a like-for-like swap. + +BEGIN; + +-- widen the status check (additive — existing values still valid) +ALTER TABLE transceiver_equivalences DROP CONSTRAINT transceiver_equivalences_status_check; +ALTER TABLE transceiver_equivalences ADD CONSTRAINT transceiver_equivalences_status_check + CHECK (status::text = ANY (ARRAY['pending','approved','rejected','auto_approved','compatible_superset']::text[])); + +UPDATE transceiver_equivalences e + SET status = 'compatible_superset', + confidence = GREATEST(e.confidence, 0.75), + match_notes = COALESCE(e.match_notes,'') || ' | sql/133: ' || + CASE WHEN fx.reach_meters > cx.reach_meters + THEN 'flexoptix ('||tip_reach_tier(fx.reach_meters)||') covers competitor requirement ('||tip_reach_tier(cx.reach_meters)||')' + ELSE 'competitor ('||tip_reach_tier(cx.reach_meters)||') covers flexoptix requirement ('||tip_reach_tier(fx.reach_meters)||')' + END || ' — reach-tier substitute, not identical spec', + updated_at = now() + FROM transceivers fx, transceivers cx + WHERE e.flexoptix_id = fx.id + AND e.competitor_id = cx.id + AND e.status = 'pending' + AND fx.form_factor = cx.form_factor + AND fx.speed_gbps = cx.speed_gbps + AND fx.fiber_type IS NOT DISTINCT FROM cx.fiber_type + AND fx.reach_meters >= 10 AND cx.reach_meters >= 10 + AND tip_extract_wavelength_nm(fx.wavelengths) IS NOT NULL + AND tip_extract_wavelength_nm(cx.wavelengths) IS NOT NULL + AND abs(tip_extract_wavelength_nm(fx.wavelengths) - tip_extract_wavelength_nm(cx.wavelengths)) <= 10 + AND tip_reach_tier(fx.reach_meters) <> tip_reach_tier(cx.reach_meters) + AND tip_reach_tier(fx.reach_meters) <> 'ZR' + AND tip_reach_tier(cx.reach_meters) <> 'ZR'; + +COMMIT;