-- 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;