-- Explicit civil-date windows. Existing results and original profile dates are never rewritten. begin; do $$ begin if current_user <> 'schema_owner' then raise exception 'dated_windows_requires_schema_owner' using errcode='42501'; end if; end $$; create or replace function public.rectification_dated_intervals(p_snapshot jsonb, p_range jsonb) returns jsonb language plpgsql immutable set search_path='' as $$ declare d date := (p_snapshot->>'birth_date')::date; s text := p_range->>'start_time'; e text := p_range->>'end_time'; reported text := left(coalesce(p_snapshot->>'reported_birth_time',''),5); a timestamp; b timestamp; begin if not public.agentic_rectification_is_clock(s) or not public.agentic_rectification_is_clock(e) then raise exception 'agentic_rectification_invalid_range'; end if; if reported='' and p_snapshot->>'birth_time_source'='period_only' and p_snapshot->>'birth_time_period'='late_night' and coalesce(p_snapshot->>'declared_window_start','')='' and coalesce(p_snapshot->>'declared_window_end','')='' and s='23:00' and e='03:59' then return jsonb_build_array(jsonb_build_object('start_at',d::text||'T00:00','end_at',d::text||'T03:59'), jsonb_build_object('start_at',d::text||'T23:00','end_at',d::text||'T23:59')); end if; a := d+s::time; b := d+e::time; if reported<>'' then -- The declared minute is the anchor even after narrowing to one side of midnight. if s::time-reported::time>interval '12 hours' then a:=a-interval '1 day'; elsif s::time-reported::timepublic.agentic_rectification_clock_inclusive_width(s,e)-1); elsif coalesce(p_snapshot->>'declared_window_start','')>coalesce(p_snapshot->>'declared_window_end','') and s>'declared_window_start' then a:=a+interval '1 day'; b:=a+make_interval(mins=>public.agentic_rectification_clock_inclusive_width(s,e)-1); elsif e>'reported_birth_time',''),5); if reported<>'' then anchor:=(c.baseline_birth_snapshot->>'birth_date')::date+reported::time; before_minutes:=case when c.baseline_birth_snapshot->>'birth_time_source'='approximate' then greatest(15,least(120,coalesce((c.baseline_birth_snapshot->>'uncertainty_before_minutes')::integer,15))) else 15 end; after_minutes:=case when c.baseline_birth_snapshot->>'birth_time_source'='approximate' then greatest(15,least(120,coalesce((c.baseline_birth_snapshot->>'uncertainty_after_minutes')::integer,15))) else 15 end; lower_at:=greatest((parts->0->>'start_at')::timestamp,anchor-make_interval(mins=>before_minutes)); upper_at:=least((parts->0->>'end_at')::timestamp,anchor+make_interval(mins=>after_minutes)); if lower_at>upper_at then raise exception 'agentic_rectification_dated_window_requires_recompare'; end if; parts:=jsonb_build_array(jsonb_build_object('start_at',to_char(lower_at,'YYYY-MM-DD"T"HH24:MI'), 'end_at',to_char(upper_at,'YYYY-MM-DD"T"HH24:MI'))); end if; r:=r||jsonb_build_object('candidate_intervals',parts); if jsonb_array_length(parts)=2 then r:=r||jsonb_build_object('midnight_side_pending',true); end if; update public.agentic_rectification_cases set candidate_range=r where id=c.id; end if; return r; end $$; create or replace function public.advance_agentic_rectification_dated_window( p_user_id uuid,p_case_id uuid,p_start_time text,p_end_time text,p_candidate_intervals jsonb,p_midnight_side_pending boolean default false ) returns jsonb language plpgsql security definer set search_path='' as $$ declare c public.agentic_rectification_cases%rowtype; parent jsonb; part jsonb; prev timestamp; a timestamp; b timestamp; total integer:=0; result jsonb; r jsonb; begin parent:=public.ensure_agentic_rectification_dated_window(p_user_id,p_case_id); select * into c from public.agentic_rectification_cases where id=p_case_id and user_id=p_user_id for update; if p_midnight_side_pending is null or ( parent->'midnight_side_pending'='true'::jsonb and p_midnight_side_pending and p_candidate_intervals is distinct from parent->'candidate_intervals' ) then raise exception 'agentic_rectification_invalid_block_window'; end if; if jsonb_typeof(p_candidate_intervals) is distinct from 'array' or jsonb_array_length(p_candidate_intervals) not between 1 and 2 then raise exception 'agentic_rectification_invalid_block_window'; end if; for part in select value from jsonb_array_elements(p_candidate_intervals) loop if coalesce(part->>'start_at','') !~ '^\d{4}-\d{2}-\d{2}T([01][0-9]|2[0-3]):[0-5][0-9]$' or coalesce(part->>'end_at','') !~ '^\d{4}-\d{2}-\d{2}T([01][0-9]|2[0-3]):[0-5][0-9]$' then raise exception 'agentic_rectification_invalid_block_window'; end if; a:=(part->>'start_at')::timestamp; b:=(part->>'end_at')::timestamp; if b'candidate_intervals') x where a>=(x->>'start_at')::timestamp and b<=(x->>'end_at')::timestamp ) then raise exception 'agentic_rectification_invalid_block_window'; end if; if exists (select 1 from generate_series(a,b,interval '1 minute') t where not public.agentic_rectification_clock_contains( p_start_time,p_end_time,to_char(t,'HH24:MI'),to_char(t,'HH24:MI'))) then raise exception 'agentic_rectification_invalid_block_window'; end if; total:=total+extract(epoch from b-a)::integer/60+1; prev:=b; end loop; if total>1440 then raise exception 'agentic_rectification_invalid_block_window'; end if; -- Delegate ownership, stage, round cap and result invalidation to the existing implementation. result:=public.advance_agentic_rectification_case_from_block_scan(p_user_id,p_case_id,p_start_time,p_end_time); r:=(result->'candidate_range')||jsonb_build_object('candidate_intervals',p_candidate_intervals); if parent ? 'midnight_side_pending' then -- Only an actual D1 choice may establish a side. Later narrowing must not -- reinterpret a surviving C/skip segment as a new user declaration. if parent->'midnight_side_pending'='true'::jsonb and not p_midnight_side_pending then if p_candidate_intervals<>parent->'candidate_intervals' and not exists ( select 1 from jsonb_array_elements(parent->'candidate_intervals') x where p_candidate_intervals=jsonb_build_array(x) ) then raise exception 'agentic_rectification_invalid_block_window'; end if; r:=r||jsonb_build_object('midnight_allowed_intervals',p_candidate_intervals); elsif parent ? 'midnight_allowed_intervals' then r:=r||jsonb_build_object('midnight_allowed_intervals',parent->'midnight_allowed_intervals'); else -- Pre-provenance windows cannot regain an unknown discarded side. r:=r||jsonb_build_object('midnight_allowed_intervals',parent->'candidate_intervals'); end if; r:=r||jsonb_build_object('midnight_side_pending', parent->'midnight_side_pending'='true'::jsonb and p_midnight_side_pending); end if; update public.agentic_rectification_cases set candidate_range=r where id=c.id; return result||jsonb_build_object('candidate_range',r); end $$; create or replace function public.widen_agentic_rectification_dated_window(p_user_id uuid,p_case_id uuid,p_start_time text,p_end_time text) returns jsonb language plpgsql security definer set search_path='' as $$ declare c public.agentic_rectification_cases%rowtype; result jsonb; r jsonb; parent jsonb; parts jsonb; allowed jsonb; a timestamp; b timestamp; width integer; total integer; begin select * into c 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; parent:=public.ensure_agentic_rectification_dated_window(p_user_id,p_case_id); -- Keep every original ownership/stage/adoption/containment/strict-width guard. -- Any later exception rolls back this call's invalidation and all row writes. result:=public.widen_agentic_rectification_case_window(p_user_id,p_case_id,p_start_time,p_end_time); width:=public.agentic_rectification_clock_inclusive_width(p_start_time,p_end_time); r:=result->'candidate_range'; if parent ? 'midnight_side_pending' then allowed:=coalesce(parent->'midnight_allowed_intervals',parent->'candidate_intervals'); -- Intersect the requested clocks with the server-persisted allowed set. -- Group by actual consecutive civil minutes, never min/max across a hole. with minutes as ( select distinct t from jsonb_array_elements(allowed) x cross join lateral generate_series((x->>'start_at')::timestamp,(x->>'end_at')::timestamp,interval '1 minute') t where public.agentic_rectification_clock_contains(p_start_time,p_end_time,to_char(t,'HH24:MI'),to_char(t,'HH24:MI')) ), numbered as ( select t,t-row_number() over(order by t)*interval '1 minute' as grp from minutes ), segments as ( select min(t) as a,max(t) as b from numbered group by grp ) select jsonb_agg(jsonb_build_object('start_at',to_char(segments.a,'YYYY-MM-DD"T"HH24:MI'), 'end_at',to_char(segments.b,'YYYY-MM-DD"T"HH24:MI')) order by segments.a) into parts from segments; r:=r||jsonb_build_object('midnight_side_pending',parent->'midnight_side_pending', 'midnight_allowed_intervals',allowed); else if jsonb_array_length(parent->'candidate_intervals')<>1 then raise exception 'agentic_rectification_invalid_widen_window'; end if; a:=(parent->'candidate_intervals'->0->>'start_at')::timestamp; -- Align the new start before the persisted start, without consulting the -- declared date again (it may be the previous/next day's narrowed window). a:=a-make_interval(mins=>public.agentic_rectification_clock_inclusive_width(p_start_time,to_char(a,'HH24:MI'))-1); b:=a+make_interval(mins=>width-1); parts:=jsonb_build_array(jsonb_build_object('start_at',to_char(a,'YYYY-MM-DD"T"HH24:MI'), 'end_at',to_char(b,'YYYY-MM-DD"T"HH24:MI'))); end if; select sum(extract(epoch from (x->>'end_at')::timestamp-(x->>'start_at')::timestamp)::integer/60+1) into total from jsonb_array_elements(parts) x; if parts is null or total is distinct from width or exists ( select 1 from jsonb_array_elements(parent->'candidate_intervals') old_part where not exists (select 1 from jsonb_array_elements(parts) new_part where (old_part->>'start_at')::timestamp>=(new_part->>'start_at')::timestamp and (old_part->>'end_at')::timestamp<=(new_part->>'end_at')::timestamp) ) then raise exception 'agentic_rectification_invalid_widen_window'; end if; r:=r||jsonb_build_object('candidate_intervals',parts); update public.agentic_rectification_cases set candidate_range=r where id=c.id; return result||jsonb_build_object('candidate_range',r); end $$; revoke all on function public.rectification_dated_intervals(jsonb,jsonb) from public,anon,authenticated; revoke all on function public.ensure_agentic_rectification_dated_window(uuid,uuid) from public,anon,authenticated; revoke all on function public.advance_agentic_rectification_dated_window(uuid,uuid,text,text,jsonb,boolean) from public,anon,authenticated; revoke all on function public.widen_agentic_rectification_dated_window(uuid,uuid,text,text) from public,anon,authenticated; grant execute on function public.rectification_dated_intervals(jsonb,jsonb) to service_role; grant execute on function public.ensure_agentic_rectification_dated_window(uuid,uuid) to service_role; grant execute on function public.advance_agentic_rectification_dated_window(uuid,uuid,text,text,jsonb,boolean) to service_role; grant execute on function public.widen_agentic_rectification_dated_window(uuid,uuid,text,text) to service_role; commit;