fix(rectification): anchor candidate windows to civil dates across midnight
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s

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.
This commit is contained in:
jesse-ux
2026-09-21 02:55:00 +08:00
parent 3be740f84d
commit b85c4a686a
115 changed files with 106484 additions and 315 deletions
@@ -0,0 +1,185 @@
-- 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;