transceiver-db/sql/103-verification-evidence-and-competitor-status.sql
2026-05-09 23:06:21 +02:00

213 lines
6.3 KiB
SQL

-- Migration 103: Verification evidence ledger and competitor status semantics
--
-- Goal:
-- fully_verified should mean "source-backed and resolved", not merely
-- "a competitor row was found". A product may be fully resolved when a
-- valid 1:1 competitor exists OR when research verified that no valid
-- public 1:1 competitor is available.
ALTER TABLE transceivers
ADD COLUMN IF NOT EXISTS competitor_status VARCHAR(32) NOT NULL DEFAULT 'unknown',
ADD COLUMN IF NOT EXISTS competitor_status_updated_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS no_match_verified_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS no_match_reason TEXT;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'transceivers_competitor_status_check'
) THEN
ALTER TABLE transceivers
ADD CONSTRAINT transceivers_competitor_status_check
CHECK (competitor_status IN (
'unknown',
'matched',
'no_valid_match',
'needs_research',
'ambiguous'
));
END IF;
END $$;
UPDATE transceivers
SET competitor_status = CASE
WHEN competitor_verified = true THEN 'matched'
WHEN competitor_status = 'unknown' THEN 'needs_research'
ELSE competitor_status
END,
competitor_status_updated_at = COALESCE(competitor_status_updated_at, NOW())
WHERE competitor_status IS NULL
OR competitor_status = 'unknown'
OR competitor_verified = true;
CREATE INDEX IF NOT EXISTS idx_transceivers_competitor_status
ON transceivers (competitor_status);
CREATE INDEX IF NOT EXISTS idx_transceivers_no_valid_match
ON transceivers (no_match_verified_at)
WHERE competitor_status = 'no_valid_match';
CREATE TABLE IF NOT EXISTS transceiver_verification_evidence (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
transceiver_id UUID NOT NULL REFERENCES transceivers(id) ON DELETE CASCADE,
verification_type VARCHAR(40) NOT NULL CHECK (verification_type IN (
'price',
'image',
'details',
'competitor_match',
'competitor_no_match',
'artifact_quarantine'
)),
source_url TEXT,
source_vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL,
evidence_value JSONB NOT NULL DEFAULT '{}'::jsonb,
evidence_hash TEXT,
robot_name TEXT NOT NULL DEFAULT 'unknown',
confidence NUMERIC(4,3) CHECK (confidence IS NULL OR confidence BETWEEN 0 AND 1),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_verification_evidence_tx
ON transceiver_verification_evidence (transceiver_id, verification_type, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedupe
ON transceiver_verification_evidence (
transceiver_id,
verification_type,
COALESCE(evidence_hash, ''),
robot_name
);
COMMENT ON COLUMN transceivers.competitor_status IS
'Resolution state for competitor evidence: matched, no_valid_match, needs_research, ambiguous, unknown.';
COMMENT ON TABLE transceiver_verification_evidence IS
'Append-only evidence ledger for TIP verification decisions. Stores source-backed proof for price, image, details, competitor matches and verified no-match states.';
-- Seed the ledger from already verified rows so TIP starts with an auditable
-- baseline instead of an empty proof table.
INSERT INTO transceiver_verification_evidence (
transceiver_id,
verification_type,
source_url,
source_vendor_id,
evidence_value,
evidence_hash,
robot_name,
confidence
)
SELECT DISTINCT ON (t.id)
t.id,
'price',
po.url,
po.source_vendor_id,
jsonb_build_object(
'price', po.price,
'currency', po.currency,
'observed_at', po.time
),
md5(jsonb_build_object(
'type', 'price',
'price', po.price,
'currency', po.currency,
'url', COALESCE(po.url, '')
)::text),
'migration:103:price-backfill',
1.0
FROM transceivers t
JOIN price_observations po ON po.transceiver_id = t.id
WHERE t.price_verified = true
AND COALESCE(po.is_verified, true) = true
ORDER BY t.id, po.time DESC
ON CONFLICT DO NOTHING;
INSERT INTO transceiver_verification_evidence (
transceiver_id,
verification_type,
source_url,
evidence_value,
evidence_hash,
robot_name,
confidence
)
SELECT
id,
'image',
COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, '')),
jsonb_build_object('image_url', COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, ''))),
md5(jsonb_build_object('type', 'image', 'url', COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, '')))::text),
'migration:103:image-backfill',
1.0
FROM transceivers
WHERE image_verified = true
AND COALESCE(NULLIF(image_verified_url, ''), NULLIF(image_url, '')) IS NOT NULL
ON CONFLICT DO NOTHING;
INSERT INTO transceiver_verification_evidence (
transceiver_id,
verification_type,
source_url,
evidence_value,
evidence_hash,
robot_name,
confidence
)
SELECT
id,
'details',
COALESCE(NULLIF(details_source_url, ''), NULLIF(product_page_url, '')),
jsonb_build_object(
'form_factor', form_factor,
'speed_gbps', speed_gbps,
'reach_label', reach_label,
'fiber_type', fiber_type
),
md5(jsonb_build_object(
'type', 'details',
'source_url', COALESCE(NULLIF(details_source_url, ''), NULLIF(product_page_url, '')),
'form_factor', form_factor,
'speed_gbps', speed_gbps,
'reach_label', reach_label,
'fiber_type', fiber_type
)::text),
'migration:103:details-backfill',
1.0
FROM transceivers
WHERE details_verified = true
AND COALESCE(NULLIF(details_source_url, ''), NULLIF(product_page_url, '')) IS NOT NULL
ON CONFLICT DO NOTHING;
INSERT INTO transceiver_verification_evidence (
transceiver_id,
verification_type,
evidence_value,
evidence_hash,
robot_name,
confidence
)
SELECT DISTINCT ON (eq.flexoptix_id)
eq.flexoptix_id,
'competitor_match',
jsonb_build_object(
'equivalence_id', eq.id,
'competitor_id', eq.competitor_id,
'status', eq.status,
'match_basis', eq.match_basis,
'match_notes', eq.match_notes
),
md5(jsonb_build_object(
'type', 'competitor_match',
'equivalence_id', eq.id,
'competitor_id', eq.competitor_id,
'status', eq.status
)::text),
'migration:103:competitor-match-backfill',
eq.confidence
FROM transceiver_equivalences eq
WHERE eq.status IN ('approved', 'auto_approved')
ORDER BY eq.flexoptix_id, eq.confidence DESC, eq.updated_at DESC
ON CONFLICT DO NOTHING;