Files
Jyotisha/frontend/supabase/migrations/20260920030000_rectification_dated_windows.sql
T
jesse-ux b85c4a686a
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s
fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock
order. Cluster width, delivery, adoption, and reports keep the actual civil
date; adopted date is stored separately from the reported birth_date.

Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation
thresholds, and Skill version are unchanged. Isolated Linux final-3 gates
passed; four pre-existing Python failures remain. This is not a production
release.
2026-09-21 02:55:00 +08:00

186 lines
13 KiB
PL/PgSQL

-- 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::time<interval '-12 hours' then a:=a+interval '1 day'; end if;
b:=a+make_interval(mins=>public.agentic_rectification_clock_inclusive_width(s,e)-1);
elsif coalesce(p_snapshot->>'declared_window_start','')>coalesce(p_snapshot->>'declared_window_end','')
and s<p_snapshot->>'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<s then b:=b+interval '1 day';
end if;
return 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 $$;
create or replace function public.ensure_agentic_rectification_dated_window(p_user_id uuid,p_case_id uuid)
returns jsonb language plpgsql security definer set search_path='' as $$
declare c public.agentic_rectification_cases%rowtype; r jsonb; parts jsonb;
reported text; anchor timestamp; lower_at timestamp; upper_at timestamp; before_minutes integer; after_minutes 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;
if c.status in ('confirmed','closed','abandoned','superseded') then raise exception 'agentic_rectification_case_terminal'; end if;
r:=c.candidate_range;
if not (r ? 'candidate_intervals') then
parts:=public.rectification_dated_intervals(c.baseline_birth_snapshot,r);
reported:=left(coalesce(c.baseline_birth_snapshot->>'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<a or (prev is not null and a<=prev) or not exists (
select 1 from jsonb_array_elements(parent->'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;