begin; create or replace function public.agentic_rectification_evidence_kinds() returns text[] language sql immutable as $$ select array[ 'education_start', 'education_completion', 'education_interruption', 'education_change', 'education_milestone', 'career_entry', 'career_change', 'promotion', 'career_pressure', 'career_exit', 'business_start', 'relationship_start', 'relationship_commitment', 'relationship_separation', 'relationship_end', 'relationship_change', 'relocation', 'foreign_move', 'return', 'home_change', 'finance_gain', 'finance_loss', 'income_change', 'asset_change', 'finance_change', 'self_health_event', 'pressure_period', 'family_event', 'other' ]::text[] $$; revoke all on function public.agentic_rectification_evidence_kinds() from public, anon, authenticated; grant execute on function public.agentic_rectification_evidence_kinds() to service_role; create or replace function public.agentic_rectification_evidence_domains() returns text[] language sql immutable as $$ select array[ 'education', 'career', 'relationship', 'relocation', 'finance', 'health', 'health_pressure', 'family', 'other' ]::text[] $$; revoke all on function public.agentic_rectification_evidence_domains() from public, anon, authenticated; grant execute on function public.agentic_rectification_evidence_domains() to service_role; create or replace function public.agentic_rectification_date_precisions() returns text[] language sql immutable as $$ select array['year', 'month', 'quarter', 'day', 'range', 'unknown']::text[] $$; revoke all on function public.agentic_rectification_date_precisions() from public, anon, authenticated; grant execute on function public.agentic_rectification_date_precisions() to service_role; create or replace function public.agentic_rectification_precision_rank(p_precision text) returns integer language sql immutable as $$ select case p_precision when 'day' then 4 when 'month' then 3 when 'quarter' then 2 when 'range' then 2 when 'year' then 1 when 'unknown' then 0 else -1 end $$; revoke all on function public.agentic_rectification_precision_rank(text) from public, anon, authenticated; grant execute on function public.agentic_rectification_precision_rank(text) to service_role; create or replace function public.agentic_rectification_normalize_quote(p_value text) returns text language sql immutable as $$ select regexp_replace( lower(coalesce(p_value, '')), '[\s\u3000,。!?、;:“”‘’()《》·—…,!.;:?]', '', 'g' ) $$; do $$ declare rec record; begin for rec in select con.conname from pg_constraint con where con.conrelid = 'public.agentic_rectification_evidence'::regclass and con.contype = 'c' and pg_get_constraintdef(con.oid) like '%event_kind%' loop execute format( 'alter table public.agentic_rectification_evidence drop constraint %I', rec.conname ); end loop; for rec in select con.conname from pg_constraint con where con.conrelid = 'public.agentic_rectification_evidence'::regclass and con.contype = 'c' and pg_get_constraintdef(con.oid) like '%domain%' and pg_get_constraintdef(con.oid) not like '%event_kind%' loop execute format( 'alter table public.agentic_rectification_evidence drop constraint %I', rec.conname ); end loop; for rec in select con.conname from pg_constraint con where con.conrelid = 'public.agentic_rectification_evidence'::regclass and con.contype = 'c' and pg_get_constraintdef(con.oid) like '%date_precision%' loop execute format( 'alter table public.agentic_rectification_evidence drop constraint %I', rec.conname ); end loop; end; $$; alter table public.agentic_rectification_evidence add constraint agentic_rectification_evidence_event_kind_check check (event_kind = any (public.agentic_rectification_evidence_kinds())); alter table public.agentic_rectification_evidence add constraint agentic_rectification_evidence_domain_check check (domain = any (public.agentic_rectification_evidence_domains())); alter table public.agentic_rectification_evidence add constraint agentic_rectification_evidence_date_precision_check check (date_precision = any (public.agentic_rectification_date_precisions())); create or replace function public.propose_agentic_rectification_evidence( p_user_id uuid, p_case_id uuid, p_source_turn_id uuid, p_user_quote text, p_subject text, p_event_kind text, p_domain text, p_occurred_from date, p_occurred_to date, p_date_precision text, p_summary text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_turn public.agentic_rectification_turns%rowtype; v_evidence_id uuid; v_existing_id uuid; begin if p_user_id is null or p_case_id is null or p_source_turn_id is null or length(btrim(coalesce(p_user_quote, ''))) = 0 or length(btrim(coalesce(p_summary, ''))) = 0 or p_subject not in ('self', 'family', 'other') then return jsonb_build_object( 'evidence_id', null, 'idempotent', false, 'outcome', 'rejected', 'error_code', 'invalid_item', 'status', 'rejected' ); end if; if p_event_kind is null or not (p_event_kind = any (public.agentic_rectification_evidence_kinds())) or p_domain is null or not (p_domain = any (public.agentic_rectification_evidence_domains())) or p_date_precision is null or not (p_date_precision = any (public.agentic_rectification_date_precisions())) then return jsonb_build_object( 'evidence_id', null, 'idempotent', false, 'outcome', 'rejected', 'error_code', 'invalid_item', 'status', 'rejected' ); end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; select * into v_turn from public.agentic_rectification_turns where id = p_source_turn_id and case_id = p_case_id; if not found then raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001'; end if; if v_turn.user_message is null or position( public.agentic_rectification_normalize_quote(p_user_quote) in public.agentic_rectification_normalize_quote(v_turn.user_message) ) = 0 then return jsonb_build_object( 'evidence_id', null, 'idempotent', false, 'outcome', 'rejected', 'error_code', 'quote_not_grounded', 'status', 'rejected' ); end if; select id into v_existing_id from public.agentic_rectification_evidence where case_id = p_case_id and source_turn_id = p_source_turn_id and user_quote = p_user_quote and event_kind = p_event_kind and summary = p_summary and status in ('draft', 'pending_confirmation') limit 1; if v_existing_id is not null then return jsonb_build_object( 'evidence_id', v_existing_id, 'idempotent', true, 'outcome', 'accepted', 'error_code', null, 'status', 'draft' ); end if; insert into public.agentic_rectification_evidence ( case_id, source_turn_id, user_quote, subject, event_kind, domain, occurred_from, occurred_to, date_precision, summary, status ) values ( p_case_id, p_source_turn_id, p_user_quote, p_subject, p_event_kind, p_domain, p_occurred_from, p_occurred_to, p_date_precision, p_summary, 'draft' ) returning id into v_evidence_id; update public.agentic_rectification_cases set last_activity_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = p_case_id; return jsonb_build_object( 'evidence_id', v_evidence_id, 'idempotent', false, 'outcome', 'accepted', 'error_code', null, 'status', 'draft' ); end; $$; create or replace function public.confirm_agentic_rectification_evidence_v10( p_user_id uuid, p_case_id uuid, p_focus_id uuid, p_evidence_id uuid ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_focus public.agentic_rectification_conversation_focuses%rowtype; v_result jsonb; begin if p_user_id is null or p_case_id is null or p_evidence_id is null then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; if not exists ( select 1 from public.agentic_rectification_evidence where id = p_evidence_id and case_id = p_case_id ) then raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001'; end if; if p_focus_id is null then v_result := public.confirm_agentic_rectification_evidence( p_user_id, p_case_id, p_evidence_id ); return v_result || jsonb_build_object('focus_id', null); end if; select * into v_focus from public.agentic_rectification_conversation_focuses where id = p_focus_id and case_id = p_case_id for update; if not found then raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001'; end if; if v_focus.target_evidence_id is not null and v_focus.target_evidence_id <> p_evidence_id then raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001'; end if; if v_focus.status not in ('active', 'resolved') then raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001'; end if; if v_focus.status = 'resolved' and v_focus.target_evidence_id is distinct from p_evidence_id then raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001'; end if; v_result := public.confirm_agentic_rectification_evidence( p_user_id, p_case_id, p_evidence_id ); if v_focus.status = 'active' and v_focus.target_evidence_id is not null then perform public.resolve_agentic_rectification_conversation_focus( p_user_id, p_case_id, p_focus_id, 'resolved', p_evidence_id ); end if; return v_result || jsonb_build_object('focus_id', p_focus_id); end; $$; create or replace function public.revise_agentic_rectification_evidence( p_user_id uuid, p_case_id uuid, p_evidence_id uuid, p_user_quote text, p_occurred_from date, p_occurred_to date, p_date_precision text, p_summary text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_target public.agentic_rectification_evidence%rowtype; v_new_id uuid; v_normalized text; v_has_day boolean; v_has_month boolean; v_has_year boolean; v_allows_coarser boolean; begin if p_user_id is null or p_case_id is null or p_evidence_id is null or length(btrim(coalesce(p_user_quote, ''))) = 0 or length(btrim(coalesce(p_summary, ''))) = 0 or p_date_precision is null or not (p_date_precision = any (public.agentic_rectification_date_precisions())) then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; select * into v_target from public.agentic_rectification_evidence where id = p_evidence_id and case_id = p_case_id; if not found then raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001'; end if; if v_target.status not in ('confirmed', 'pending_confirmation') then raise exception 'agentic_rectification_evidence_not_revisable' using errcode = 'P0001'; end if; v_normalized := public.agentic_rectification_normalize_quote(p_user_quote); if public.agentic_rectification_precision_rank(p_date_precision) < public.agentic_rectification_precision_rank(v_target.date_precision) then v_has_day := v_normalized ~ '[0-9]{1,2}月[0-9]{1,2}[日号]' or coalesce(p_user_quote, '') ~ '[0-9]{4}-[0-9]{2}-[0-9]{2}'; v_has_month := v_normalized ~ '[0-9]{1,2}月' or coalesce(p_user_quote, '') ~ '[0-9]{4}-[0-9]{2}'; v_has_year := v_normalized ~ '[0-9]{4}年' or coalesce(p_user_quote, '') ~ '[0-9]{4}'; v_allows_coarser := case p_date_precision when 'year' then v_has_year and not v_has_month and not v_has_day when 'month' then v_has_month and not v_has_day when 'quarter' then v_has_month and not v_has_day when 'unknown' then v_normalized ~ '(不记得|记不清|不明|不知道)' else false end; if not v_allows_coarser then raise exception 'agentic_rectification_precision_downgrade' using errcode = 'P0001'; end if; end if; update public.agentic_rectification_evidence set status = 'superseded', updated_at = pg_catalog.now() where id = v_target.id; insert into public.agentic_rectification_evidence ( case_id, source_turn_id, user_quote, subject, event_kind, domain, occurred_from, occurred_to, date_precision, summary, status, supersedes_evidence_id ) values ( v_target.case_id, v_target.source_turn_id, p_user_quote, v_target.subject, v_target.event_kind, v_target.domain, p_occurred_from, p_occurred_to, p_date_precision, p_summary, 'pending_confirmation', v_target.id ) returning id into v_new_id; update public.agentic_rectification_cases set last_activity_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = p_case_id; return jsonb_build_object( 'evidence_id', v_new_id, 'supersedes_evidence_id', v_target.id, 'idempotent', false ); end; $$; create or replace function public.record_agentic_rectification_evidence_batch( p_user_id uuid, p_case_id uuid, p_source_turn_id uuid, p_focus_id uuid, p_items jsonb ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_turn public.agentic_rectification_turns%rowtype; v_focus public.agentic_rectification_conversation_focuses%rowtype; v_focus_replay boolean := false; v_focus_match_id uuid; v_focus_match_count integer := 0; v_focus_resolution text := 'not_requested'; v_item jsonb; v_index bigint; v_item_key text; v_quote text; v_subject text; v_kind text; v_domain text; v_precision text; v_summary text; v_from date; v_to date; v_status text; v_outcome text; v_error text; v_clarification jsonb; v_existing public.agentic_rectification_evidence%rowtype; v_evidence_id uuid; v_idempotent boolean; v_results jsonb := '[]'::jsonb; v_accepted integer := 0; v_needs integer := 0; v_rejected integer := 0; begin if p_user_id is null or p_case_id is null or p_source_turn_id is null or p_items is null or jsonb_typeof(p_items) <> 'array' or jsonb_array_length(p_items) = 0 or jsonb_array_length(p_items) > 12 then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; select * into v_turn from public.agentic_rectification_turns where id = p_source_turn_id and case_id = p_case_id; if not found then raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001'; end if; if v_turn.user_message is null then raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001'; end if; if p_focus_id is not null then select * into v_focus from public.agentic_rectification_conversation_focuses where id = p_focus_id and case_id = p_case_id for update; if not found or v_focus.status not in ('active', 'resolved') then raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001'; end if; v_focus_replay := v_focus.status = 'resolved'; if not v_focus_replay and v_focus.target_evidence_id is not null then raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001'; end if; v_focus_resolution := case when v_focus_replay then 'idempotent' else 'active' end; if v_focus_replay and exists ( select 1 from jsonb_array_elements(p_items) item where length(btrim(coalesce(item->>'idempotency_key', ''))) = 0 or not exists ( select 1 from public.agentic_rectification_evidence e where e.case_id = p_case_id and e.idempotency_key = btrim(item->>'idempotency_key') ) ) then raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001'; end if; end if; for v_item, v_index in select value, ordinality from jsonb_array_elements(p_items) with ordinality loop v_item_key := btrim(coalesce(v_item->>'idempotency_key', '')); v_quote := btrim(coalesce(v_item->>'quote', '')); v_subject := coalesce(v_item->>'subject', ''); v_kind := coalesce(v_item->>'event_kind', ''); v_domain := coalesce(v_item->>'domain', ''); v_precision := coalesce(v_item->>'date_precision', ''); v_summary := btrim(coalesce(v_item->>'summary', '')); v_from := null; v_to := null; v_error := null; v_clarification := '[]'::jsonb; v_evidence_id := null; v_idempotent := false; begin if nullif(v_item->>'occurred_from', '') is not null then v_from := (v_item->>'occurred_from')::date; end if; if nullif(v_item->>'occurred_to', '') is not null then v_to := (v_item->>'occurred_to')::date; end if; exception when others then v_error := 'invalid_date'; end; if v_error is null and ( length(v_item_key) = 0 or length(v_item_key) > 160 or length(v_quote) = 0 or length(v_summary) = 0 or v_subject not in ('self', 'family', 'other') or not (v_kind = any (public.agentic_rectification_evidence_kinds())) or not (v_domain = any (public.agentic_rectification_evidence_domains())) or not (v_precision = any (public.agentic_rectification_date_precisions())) ) then v_error := 'invalid_item'; end if; if v_error is null and position( public.agentic_rectification_normalize_quote(v_quote) in public.agentic_rectification_normalize_quote(v_turn.user_message) ) = 0 then v_error := 'quote_not_grounded'; end if; if v_error is null and v_precision = 'range' and (v_from is null or v_to is null or v_from > v_to) then v_error := 'invalid_range'; end if; if v_error is not null then v_outcome := 'rejected'; v_status := 'rejected'; v_rejected := v_rejected + 1; else select * into v_existing from public.agentic_rectification_evidence where case_id = p_case_id and idempotency_key = v_item_key; if found then if v_existing.source_turn_id is distinct from p_source_turn_id or v_existing.user_quote is distinct from v_quote or v_existing.subject is distinct from v_subject or v_existing.event_kind is distinct from v_kind or v_existing.domain is distinct from v_domain or v_existing.occurred_from is distinct from v_from or v_existing.occurred_to is distinct from v_to or v_existing.date_precision is distinct from v_precision or v_existing.summary is distinct from v_summary then v_outcome := 'rejected'; v_status := 'rejected'; v_error := 'idempotency_conflict'; v_rejected := v_rejected + 1; else v_evidence_id := v_existing.id; v_idempotent := true; if v_existing.status = 'confirmed' then v_outcome := 'accepted'; v_status := 'confirmed'; v_accepted := v_accepted + 1; else v_outcome := 'needs_clarification'; v_status := v_existing.status; v_clarification := case when v_existing.date_precision = 'unknown' then '["date"]'::jsonb else '[]'::jsonb end; v_needs := v_needs + 1; end if; end if; else if v_precision = 'unknown' or v_from is null then v_outcome := 'needs_clarification'; v_status := 'draft'; v_clarification := '["date"]'::jsonb; v_needs := v_needs + 1; else v_outcome := 'accepted'; v_status := 'confirmed'; v_accepted := v_accepted + 1; end if; insert into public.agentic_rectification_evidence ( case_id, source_turn_id, user_quote, subject, event_kind, domain, occurred_from, occurred_to, date_precision, summary, status, confirmed_at, idempotency_key ) values ( p_case_id, p_source_turn_id, v_quote, v_subject, v_kind, v_domain, v_from, v_to, v_precision, v_summary, v_status, case when v_status = 'confirmed' then pg_catalog.now() else null end, v_item_key ) returning id into v_evidence_id; end if; end if; if p_focus_id is not null and v_outcome = 'accepted' and v_evidence_id is not null and (v_focus.target_domain is null or v_domain = v_focus.target_domain) and (v_focus.target_kind is null or v_kind = v_focus.target_kind) then v_focus_match_count := v_focus_match_count + 1; if v_focus_match_id is null then v_focus_match_id := v_evidence_id; end if; end if; v_results := v_results || jsonb_build_array(jsonb_build_object( 'index', v_index - 1, 'idempotency_key', nullif(v_item_key, ''), 'outcome', v_outcome, 'evidence_id', v_evidence_id, 'status', v_status, 'idempotent', v_idempotent, 'clarification_fields', v_clarification, 'error_code', v_error )); end loop; update public.agentic_rectification_cases set status = case when status = 'draft' and v_accepted > 0 then 'collecting_evidence' else status end, last_activity_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = p_case_id; if p_focus_id is not null then if v_focus_replay then if v_focus_match_count <> 1 or v_focus_match_id is distinct from v_focus.target_evidence_id then raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001'; end if; v_focus_resolution := 'idempotent'; elsif v_focus_match_count = 1 then perform public.resolve_agentic_rectification_conversation_focus( p_user_id, p_case_id, p_focus_id, 'resolved', v_focus_match_id ); v_focus_resolution := 'resolved'; elsif v_focus_match_count > 1 then v_focus_resolution := 'ambiguous'; else v_focus_resolution := 'unmatched'; end if; end if; return jsonb_build_object( 'items', v_results, 'accepted_count', v_accepted, 'needs_clarification_count', v_needs, 'rejected_count', v_rejected, 'focus_id', p_focus_id, 'focus_evidence_id', v_focus_match_id, 'focus_resolution', v_focus_resolution ); end; $$; commit;