fix(equivalences): one-time triage of the 289,681-row pending backlog
Set-based port of scheduler.ts evaluateEquivalenceResearch(), applied once live to every pending row with re_research_due_at IS NULL (the root cause this pairs with a code fix for, previous commit). Result: 41,963 approved, 187,599 rejected as technical mismatch, 57,683 rejected as insufficient evidence (re-research in 21 days), 2,436 rejected for no recent competitor price. Backup of pre-triage state in equivalence_pending_triage_backup_20260713.
This commit is contained in:
parent
9320a236c8
commit
723dfe7bdc
130
sql/139-equivalence-pending-triage.sql
Normal file
130
sql/139-equivalence-pending-triage.sql
Normal file
@ -0,0 +1,130 @@
|
||||
-- sql/139-equivalence-pending-triage.sql
|
||||
-- Set-based port of scheduler.ts's evaluateEquivalenceResearch() (line ~85),
|
||||
-- applied once to the 289,681-row pending backlog in transceiver_equivalences.
|
||||
--
|
||||
-- Root cause of the backlog: the daily "maintenance:re-research-equivalences" job
|
||||
-- (scheduler.ts:3117) only evaluates rows where re_research_due_at IS NOT NULL AND
|
||||
-- <= NOW(). catalog-reconcile.ts inserts new 'pending' rows WITHOUT setting
|
||||
-- re_research_due_at (NULL by default) -> the job that was designed to drain the
|
||||
-- queue never saw fresh pending rows. This migration (a) triages the existing
|
||||
-- backlog once, set-based, using the exact same scoring rules as the JS
|
||||
-- evaluator, and (b) is paired with code fixes (catalog-reconcile.ts sets
|
||||
-- re_research_due_at on insert; scheduler.ts's reject branch preserves it for
|
||||
-- "insufficient evidence" rejects) so the daily job keeps draining future inserts
|
||||
-- on its own.
|
||||
--
|
||||
-- Decision rules (identical to evaluateEquivalenceResearch, ported 1:1):
|
||||
-- no recent competitor price observation (45d) -> reject, no re-research
|
||||
-- form_factor or speed_gbps mismatch/missing -> reject, no re-research
|
||||
-- wavelength diff >15nm / fiber mismatch / reach -> reject, no re-research
|
||||
-- ratio <0.85 (all three "hard" if evidence exists)
|
||||
-- wavelength / fiber_type / reach_meters MISSING -> reject, WITH re-research
|
||||
-- (on either side, and no hard mismatch above) in 21 days (data-gap,
|
||||
-- not a wrong match --
|
||||
-- give scrapers time to
|
||||
-- enrich, then recheck)
|
||||
-- confidence (score/115) >= 0.73 -> auto_approved
|
||||
-- else -> reject, no re-research
|
||||
--
|
||||
-- Idempotent for its WHERE clause (status='pending' AND re_research_due_at IS
|
||||
-- NULL); re-running after the code fix lands is a no-op since new pending rows
|
||||
-- will already carry a re_research_due_at.
|
||||
|
||||
create table if not exists equivalence_pending_triage_backup_20260713 as
|
||||
select *, now() as backed_up_at
|
||||
from transceiver_equivalences
|
||||
where status = 'pending' and re_research_due_at is null;
|
||||
|
||||
with normalized as (
|
||||
select eq.id,
|
||||
nullif(upper(trim(fx.form_factor)),'') as fx_form, nullif(upper(trim(cp.form_factor)),'') as cp_form,
|
||||
fx.speed_gbps as fx_speed, cp.speed_gbps as cp_speed,
|
||||
nullif(upper(trim(fx.standard_name)),'') as fx_std, nullif(upper(trim(cp.standard_name)),'') as cp_std,
|
||||
nullif(upper(trim(fx.fiber_type)),'') as fx_fiber, nullif(upper(trim(cp.fiber_type)),'') as cp_fiber,
|
||||
fx.reach_meters as fx_reach, cp.reach_meters as cp_reach,
|
||||
(regexp_match(fx.wavelengths, '(\d{3,4})'))[1]::numeric as fx_nm,
|
||||
(regexp_match(cp.wavelengths, '(\d{3,4})'))[1]::numeric as cp_nm,
|
||||
exists(select 1 from price_observations po where po.transceiver_id=eq.competitor_id and po.time > now() - interval '45 days') as has_recent_price
|
||||
from transceiver_equivalences eq
|
||||
join transceivers fx on fx.id = eq.flexoptix_id
|
||||
join transceivers cp on cp.id = eq.competitor_id
|
||||
where eq.status = 'pending' and eq.re_research_due_at is null
|
||||
),
|
||||
scored as (
|
||||
select *,
|
||||
(case when fx_form is not null and cp_form is not null and fx_form=cp_form then 25 else 0 end
|
||||
+ case when fx_speed is not null and cp_speed is not null and fx_speed=cp_speed then 20 else 0 end
|
||||
+ case when fx_std is not null and cp_std is not null and fx_std=cp_std then 30 else 0 end
|
||||
+ case when fx_nm is not null and cp_nm is not null then
|
||||
case when abs(fx_nm-cp_nm)<=15 then 20 else -20 end
|
||||
else 0 end
|
||||
+ case when fx_fiber is not null and cp_fiber is not null then
|
||||
case when fx_fiber=cp_fiber then 10 else -15 end
|
||||
else 0 end
|
||||
+ case when fx_reach is not null and cp_reach is not null and fx_reach>0 and cp_reach>0 then
|
||||
case when least(fx_reach,cp_reach)/greatest(fx_reach,cp_reach) >= 0.85 then 10 else -15 end
|
||||
else 0 end) as raw_score,
|
||||
(fx_nm is null or cp_nm is null) as wl_missing,
|
||||
(fx_fiber is null or cp_fiber is null) as fiber_missing,
|
||||
(fx_reach is null or cp_reach is null or fx_reach<=0 or cp_reach<=0) as reach_missing,
|
||||
(fx_nm is not null and cp_nm is not null and abs(fx_nm-cp_nm)>15) as wl_mismatch,
|
||||
(fx_fiber is not null and cp_fiber is not null and fx_fiber<>cp_fiber) as fiber_mismatch,
|
||||
(fx_reach is not null and cp_reach is not null and fx_reach>0 and cp_reach>0 and least(fx_reach,cp_reach)/greatest(fx_reach,cp_reach) < 0.85) as reach_mismatch,
|
||||
(fx_form is null or cp_form is null or fx_form<>cp_form) as form_bad,
|
||||
(fx_speed is null or cp_speed is null or fx_speed<>cp_speed) as speed_bad
|
||||
from normalized
|
||||
),
|
||||
decided as (
|
||||
select *,
|
||||
greatest(0, least(1, raw_score::numeric/115)) as confidence,
|
||||
(not has_recent_price) as no_price,
|
||||
(wl_mismatch or fiber_mismatch or reach_mismatch or form_bad or speed_bad) as critical_mismatch,
|
||||
(wl_missing or fiber_missing or reach_missing) as missing_evidence
|
||||
from scored
|
||||
)
|
||||
update transceiver_equivalences eq
|
||||
set status = case
|
||||
when d.no_price then 'rejected'
|
||||
when d.critical_mismatch then 'rejected'
|
||||
when d.missing_evidence then 'rejected'
|
||||
when d.confidence >= 0.73 then 'auto_approved'
|
||||
else 'rejected'
|
||||
end,
|
||||
confidence = case when d.no_price then 0 else d.confidence end,
|
||||
reviewed_by = 'automated-research-bulk-139',
|
||||
reviewed_at = now(),
|
||||
reject_reason = case
|
||||
when d.no_price then 'automated research: competitor has no recent price observation'
|
||||
when d.critical_mismatch then 'automated research: technical mismatch'
|
||||
when d.missing_evidence then 'automated research: insufficient technical evidence'
|
||||
when d.confidence >= 0.73 then null
|
||||
else 'automated research: confidence below approval threshold'
|
||||
end,
|
||||
re_research_due_at = case
|
||||
when (not d.no_price) and (not d.critical_mismatch) and d.missing_evidence and d.confidence < 0.73
|
||||
then now() + interval '21 days'
|
||||
else null
|
||||
end,
|
||||
re_researched_at = now(),
|
||||
match_notes = case when eq.match_notes is null or eq.match_notes = ''
|
||||
then 'bulk-triage(139) ' || now()::date
|
||||
else eq.match_notes || ' | bulk-triage(139) ' || now()::date end,
|
||||
updated_at = now()
|
||||
from decided d
|
||||
where eq.id = d.id;
|
||||
|
||||
-- Mirror the daily job's side effect on approve: mark the Flexoptix product as
|
||||
-- competitor_verified (matches scheduler.ts's confirm branch).
|
||||
update transceivers t
|
||||
set competitor_verified = true,
|
||||
competitor_verified_at = coalesce(t.competitor_verified_at, now())
|
||||
from transceiver_equivalences eq
|
||||
where eq.flexoptix_id = t.id
|
||||
and eq.status = 'auto_approved'
|
||||
and eq.reviewed_by = 'automated-research-bulk-139'
|
||||
and eq.reviewed_at > now() - interval '5 minutes'
|
||||
and t.competitor_verified = false;
|
||||
|
||||
insert into _migrations (filename, applied_at)
|
||||
select '139-equivalence-pending-triage.sql', now()
|
||||
where not exists (select 1 from _migrations where filename = '139-equivalence-pending-triage.sql');
|
||||
Loading…
x
Reference in New Issue
Block a user