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.
186 lines
13 KiB
PL/PgSQL
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;
|