begin; -- Nullable for historical Cases: only new Cases opt into segment opening checks. alter table public.agentic_rectification_cases add column if not exists rectification_domain text, add column if not exists checks jsonb; create or replace function public.initialize_agentic_rectification_domain_v1(p_user_id uuid, p_case_id uuid, p_domain text) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; begin if p_domain is null or p_domain not in ('general','report','education','career','relationship','marriage','relocation','finance','health','family','other') then raise exception 'agentic_rectification_invalid_domain'; 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'; end if; if v_case.rectification_domain is not null then return jsonb_build_object('domain',v_case.rectification_domain); end if; if v_case.status <> 'draft' or exists(select 1 from public.agentic_rectification_turns where case_id=p_case_id) then raise exception 'agentic_rectification_domain_locked'; end if; update public.agentic_rectification_cases set rectification_domain=p_domain where id=p_case_id; return jsonb_build_object('domain',p_domain); end $$; -- Owner-side read for the invoker wrapper below: runtime roles have no direct table access. create or replace function public.agentic_rectification_opening_pending_v1(p_user_id uuid, p_case_id uuid) returns boolean language sql stable security definer set search_path = '' as $$ select exists(select 1 from public.agentic_rectification_cases c where c.id=p_case_id and c.user_id=p_user_id and c.rectification_domain is not null and c.status='draft' and not exists(select 1 from public.agentic_rectification_turns t where t.case_id=c.id)); $$; -- The extra server-owned argument selects this overload; old 11-argument callers -- and exact-session opens keep their original immutable Skill/domain contract. -- SECURITY INVOKER: the immutable-skill ACL reconciliation leaves EXECUTE on the -- 11-argument open only to service_role (not the owner), so a definer wrapper -- would hit 42501. The caller is service_role, which may run both inner functions. create or replace function public.open_agentic_rectification_case_v2( p_user_id uuid, p_request_id uuid, p_intent text, p_session_id uuid, p_skill_name text, p_skill_version text, p_skill_sha256 text, p_skill_source_commit text, p_baseline_profile_fingerprint text, p_baseline_birth_snapshot jsonb, p_candidate_range jsonb, p_product_domain text ) returns jsonb language plpgsql security invoker set search_path = '' as $$ declare v_result jsonb; begin if p_product_domain is null or p_product_domain not in ('general','report','education','career','relationship','marriage','relocation','finance','health','family','other') then raise exception 'agentic_rectification_invalid_domain'; end if; v_result := public.open_agentic_rectification_case_v2(p_user_id,p_request_id,p_intent,p_session_id, p_skill_name,p_skill_version,p_skill_sha256,p_skill_source_commit,p_baseline_profile_fingerprint, p_baseline_birth_snapshot,p_candidate_range); if v_result->>'disposition' = 'created' then perform public.initialize_agentic_rectification_domain_v1(p_user_id,(v_result->>'case_id')::uuid,p_product_domain); end if; if public.agentic_rectification_opening_pending_v1(p_user_id,(v_result->>'case_id')::uuid) then v_result := v_result || jsonb_build_object('should_start_opening',true); end if; return v_result; end $$; create or replace function public.write_agentic_rectification_segment_checks_v1(p_user_id uuid, p_case_id uuid, p_candidate_range jsonb, p_checks jsonb) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_targets jsonb; v_summary jsonb; v_chart jsonb; v_segment jsonb; v_width integer; v_next integer; v_key integer; v_unique boolean := true; v_chart_unique boolean; v_original_width integer; v_origin timestamp; v_end timestamp; v_start integer; v_finish integer; begin 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'; end if; if v_case.rectification_domain is null or v_case.status not in ('draft','collecting_evidence','candidate_ready','paused') or v_case.candidate_range is distinct from p_candidate_range then raise exception 'agentic_rectification_segment_checks_stale'; end if; if v_case.checks->>'range_fingerprint' = p_checks->>'range_fingerprint' and v_case.checks->'candidate_range' = v_case.candidate_range then return v_case.checks; end if; v_targets := case v_case.rectification_domain when 'relationship' then '["D1","D9"]'::jsonb when 'marriage' then '["D1","D9"]'::jsonb when 'career' then '["D1","D10"]'::jsonb else '["D1","D9","D10"]'::jsonb end; if p_checks->>'contract' is distinct from 'segment-opening-v1' or p_checks->>'domain' is distinct from v_case.rectification_domain or coalesce(p_checks->>'range_fingerprint','') !~ '^[0-9a-f]{64}$' or coalesce(p_checks->>'status','') not in ('complete','unavailable') then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_summary := p_checks->'summary'; if p_checks->>'status' = 'unavailable' then if v_summary is distinct from 'null'::jsonb then raise exception 'agentic_rectification_invalid_segment_checks'; end if; else if v_summary->'targets' is distinct from v_targets or v_summary->'scan_complete' is distinct from 'true'::jsonb or jsonb_typeof(v_summary->'charts') is distinct from 'array' or jsonb_array_length(v_summary->'charts') <> jsonb_array_length(v_targets) or coalesce(v_summary->>'window_minutes','') !~ '^[0-9]+$' then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_width := (v_summary->>'window_minutes')::integer; if v_width < 1 or v_width > 1440 then raise exception 'agentic_rectification_invalid_segment_checks'; end if; -- Verify the original persisted window, never a credible/candidate subwindow. if v_case.candidate_range ? 'candidate_intervals' then if jsonb_typeof(v_case.candidate_range->'candidate_intervals') is distinct from 'array' or jsonb_array_length(v_case.candidate_range->'candidate_intervals') <> 1 then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_origin := (v_case.candidate_range->'candidate_intervals'->0->>'start_at')::timestamp; v_end := (v_case.candidate_range->'candidate_intervals'->0->>'end_at')::timestamp; if to_char(v_origin,'YYYY-MM-DD"T"HH24:MI') is distinct from v_case.candidate_range->'candidate_intervals'->0->>'start_at' or to_char(v_end,'YYYY-MM-DD"T"HH24:MI') is distinct from v_case.candidate_range->'candidate_intervals'->0->>'end_at' then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_original_width := extract(epoch from (v_end-v_origin))/60+1; else if coalesce(v_case.candidate_range->>'start_time','') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' or coalesce(v_case.candidate_range->>'end_time','') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' or v_case.candidate_range->'midnight_side_pending' = 'true'::jsonb then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_start := extract(hour from (v_case.candidate_range->>'start_time')::time)::integer*60 + extract(minute from (v_case.candidate_range->>'start_time')::time)::integer; v_finish := extract(hour from (v_case.candidate_range->>'end_time')::time)::integer*60 + extract(minute from (v_case.candidate_range->>'end_time')::time)::integer; v_original_width := (v_finish-v_start+1440)%1440+1; end if; if v_original_width is distinct from v_width then raise exception 'agentic_rectification_invalid_segment_checks'; end if; for v_next in 0..jsonb_array_length(v_targets)-1 loop v_chart := v_summary->'charts'->v_next; if v_chart->'chart' is distinct from v_targets->v_next or jsonb_typeof(v_chart->'segments') is distinct from 'array' or jsonb_array_length(v_chart->'segments') < 1 then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_chart_unique := jsonb_array_length(v_chart->'segments')=1; v_key := 0; for v_segment in select value from jsonb_array_elements(v_chart->'segments') loop if coalesce(v_segment->>'start','') !~ '^[0-9]+$' or coalesce(v_segment->>'end','') !~ '^[0-9]+$' or (v_segment->>'start')::integer <> v_key or (v_segment->>'end')::integer < v_key or (v_segment->>'end')::integer >= v_width or jsonb_typeof(v_segment->'key') is distinct from 'array' or jsonb_array_length(v_segment->'key')<>1 or coalesce(v_segment->'key'->>0,'') !~ '^[0-9]+$' or (v_segment->'key'->>0)::integer not between 0 and 11 then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_key := (v_segment->>'end')::integer + 1; end loop; if v_key <> v_width or v_chart->'full_window_unique' is distinct from to_jsonb(v_chart_unique) then raise exception 'agentic_rectification_invalid_segment_checks'; end if; v_unique := v_unique and v_chart_unique; end loop; if v_summary->'no_rectification_needed' is distinct from to_jsonb(v_unique) then raise exception 'agentic_rectification_invalid_segment_checks'; end if; end if; p_checks := (p_checks-'delivered_turn_id') || jsonb_build_object('candidate_range',v_case.candidate_range); update public.agentic_rectification_cases set checks=p_checks where id=p_case_id; return p_checks; end $$; -- Called only by the shared successful-turn exit. Turn + checks + close are atomic. create or replace function public.finalize_agentic_rectification_segment_consistency_v1(p_user_id uuid, p_case_id uuid, p_range_fingerprint text, p_narration text) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_turn jsonb; begin 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'; end if; if v_case.checks->>'range_fingerprint' is distinct from p_range_fingerprint or v_case.checks->'summary'->'no_rectification_needed' is distinct from 'true'::jsonb or v_case.checks->>'status' is distinct from 'complete' or v_case.checks->'candidate_range' is distinct from v_case.candidate_range or length(btrim(coalesce(p_narration,'')))=0 then raise exception 'agentic_rectification_segment_checks_stale'; end if; if v_case.checks->>'delivered_turn_id' is not null then return jsonb_build_object('turn_id',v_case.checks->>'delivered_turn_id','idempotent',true); end if; if v_case.status <> 'draft' or exists(select 1 from public.agentic_rectification_turns where case_id=p_case_id) or exists(select 1 from public.agentic_rectification_conversation_focuses where case_id=p_case_id and status='active') then raise exception 'agentic_rectification_segment_opening_started'; end if; v_turn := public.append_agentic_rectification_turn(p_user_id,p_case_id,null,p_narration,'deterministic:segment-consistency',null,'completed',p_case_id); update public.agentic_rectification_cases set checks=checks||jsonb_build_object('delivered_turn_id',v_turn->>'turn_id'), status='closed',closed_reason='other',completed_at=pg_catalog.now() where id=p_case_id; return v_turn; end $$; -- Preserve the previous implementation intact; add only two server-owned fields. do $$ begin if pg_catalog.to_regprocedure('public.get_agentic_rectification_case_dossier_before_segments(uuid,uuid)') is null then execute pg_catalog.replace(pg_catalog.pg_get_functiondef('public.get_agentic_rectification_case_dossier(uuid,uuid)'::regprocedure), 'public.get_agentic_rectification_case_dossier(', 'public.get_agentic_rectification_case_dossier_before_segments('); end if; end $$; create or replace function public.get_agentic_rectification_case_dossier(p_user_id uuid,p_case_id uuid) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_dossier jsonb; v_case public.agentic_rectification_cases%rowtype; begin v_dossier := public.get_agentic_rectification_case_dossier_before_segments(p_user_id,p_case_id); select * into v_case from public.agentic_rectification_cases where id=p_case_id and user_id=p_user_id; return jsonb_set(v_dossier,'{case}',(v_dossier->'case')||jsonb_build_object('rectification_domain',v_case.rectification_domain,'checks',v_case.checks)); end $$; revoke all on function public.get_agentic_rectification_case_dossier_before_segments(uuid,uuid) from public,anon,authenticated,service_role; revoke all on function public.open_agentic_rectification_case_v2(uuid,uuid,text,uuid,text,text,text,text,text,jsonb,jsonb,text), public.agentic_rectification_opening_pending_v1(uuid,uuid), public.initialize_agentic_rectification_domain_v1(uuid,uuid,text), public.write_agentic_rectification_segment_checks_v1(uuid,uuid,jsonb,jsonb), public.finalize_agentic_rectification_segment_consistency_v1(uuid,uuid,text,text), public.get_agentic_rectification_case_dossier(uuid,uuid) from public,anon,authenticated; grant execute on function public.open_agentic_rectification_case_v2(uuid,uuid,text,uuid,text,text,text,text,text,jsonb,jsonb,text), public.agentic_rectification_opening_pending_v1(uuid,uuid), public.initialize_agentic_rectification_domain_v1(uuid,uuid,text), public.write_agentic_rectification_segment_checks_v1(uuid,uuid,jsonb,jsonb), public.finalize_agentic_rectification_segment_consistency_v1(uuid,uuid,text,text), public.get_agentic_rectification_case_dossier(uuid,uuid) to service_role; commit;