- open_agentic_rectification_case_v2 (12 args) becomes SECURITY INVOKER: the immutable-skill ACL reconciliation leaves EXECUTE on the 11-arg open only to service_role, so the definer wrapper hit 42501 on every homepage/new open. - The pending-opening read moves to owner function agentic_rectification_opening_pending_v1, granted to service_role only. - History test and persisted replay harness append turns through the V10 request-idempotent overload; the V9 overload is revoked from service_role. - Opening test fixture adds the required birth_time_source. - Segment migrations renamed to 20261001* so they sort after staging's 20260930* migrations on both fresh and existing databases. - Stale Windows replay replaced with the production-path replay (M1/M2 equal to accepted research, implementation_identity included). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
441 lines
30 KiB
PL/PgSQL
441 lines
30 KiB
PL/PgSQL
-- A segment minute is an adoption output, not a new engine candidate or confirmation.
|
|
begin;
|
|
|
|
create function public.rectification_segment_adoption_minute(
|
|
p_case public.agentic_rectification_cases,
|
|
p_result public.agentic_rectification_results,
|
|
p_state jsonb
|
|
) returns jsonb language plpgsql security definer set search_path = '' as $$
|
|
declare
|
|
v_summary jsonb := p_state -> 'segment_summary';
|
|
v_minutes jsonb := p_state -> 'segment_minutes';
|
|
v_base jsonb := p_result.decision_receipt -> 'inference_state';
|
|
v_targets jsonb;
|
|
v_chart jsonb;
|
|
v_segments jsonb;
|
|
v_shares jsonb;
|
|
v_weights jsonb;
|
|
v_candidate jsonb;
|
|
v_payload jsonb;
|
|
v_minute jsonb;
|
|
v_top integer;
|
|
v_alive integer;
|
|
v_share double precision;
|
|
v_tied boolean;
|
|
v_sign integer;
|
|
v_tier text;
|
|
v_unique boolean;
|
|
v_all_unique boolean := true;
|
|
v_width integer;
|
|
v_origin timestamp;
|
|
v_end timestamp;
|
|
v_reported integer;
|
|
v_start_clock integer;
|
|
v_delta integer;
|
|
v_start integer;
|
|
v_finish integer;
|
|
v_intersection_start integer := 0;
|
|
v_intersection_end integer;
|
|
v_fallback_start integer;
|
|
v_fallback_end integer;
|
|
v_priority integer := 4;
|
|
v_current_priority integer;
|
|
v_midpoint integer;
|
|
v_adoption jsonb;
|
|
v_peak double precision;
|
|
v_index integer;
|
|
begin
|
|
if jsonb_typeof(v_summary) is distinct from 'object'
|
|
or v_summary -> 'scan_complete' is distinct from 'true'::jsonb
|
|
or p_state -> 'segment_scan_complete' is distinct from 'true'::jsonb
|
|
or jsonb_typeof(v_minutes) is distinct from 'array'
|
|
or jsonb_typeof(v_summary -> 'targets') is distinct from 'array'
|
|
or jsonb_typeof(v_summary -> 'charts') is distinct from 'array'
|
|
or coalesce(v_summary ->> 'window_minutes', '') !~ '^[0-9]+$' then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
v_width := (v_summary ->> 'window_minutes')::integer;
|
|
v_targets := v_summary -> 'targets';
|
|
if v_width < 1 or v_width > 1440 or jsonb_array_length(v_minutes) <> v_width
|
|
or jsonb_array_length(v_targets) not between 1 and 3
|
|
or jsonb_array_length(v_summary -> 'charts') <> jsonb_array_length(v_targets)
|
|
or p_state -> 'segment_targets' is distinct from v_targets
|
|
or v_minutes is distinct from v_base -> 'segment_minutes'
|
|
or v_targets is distinct from v_base -> 'segment_targets'
|
|
or (select count(distinct value) from jsonb_array_elements(v_targets)) <> jsonb_array_length(v_targets)
|
|
or exists (select 1 from jsonb_array_elements_text(v_targets) x where x not in ('D1','D9','D10')) then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
|
|
-- A complete proof covers one contiguous, dated window; disjoint/24h identities fail closed.
|
|
if p_result.decision_receipt ->> 'candidate_window_contract' = 'dated-v1' then
|
|
if jsonb_array_length(p_result.decision_receipt -> 'candidate_intervals') <> 1
|
|
or p_result.decision_receipt -> 'candidate_intervals' is distinct from p_case.candidate_range -> 'candidate_intervals' then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
v_origin := (p_result.decision_receipt -> 'candidate_intervals' -> 0 ->> 'start_at')::timestamp;
|
|
v_end := (p_result.decision_receipt -> 'candidate_intervals' -> 0 ->> 'end_at')::timestamp;
|
|
else
|
|
v_start_clock := extract(hour from (p_state ->> 'range_start')::time)::integer * 60
|
|
+ extract(minute from (p_state ->> 'range_start')::time)::integer;
|
|
v_origin := (p_case.baseline_birth_snapshot ->> 'birth_date')::date + make_interval(mins => v_start_clock);
|
|
if p_case.baseline_birth_snapshot ->> 'reported_birth_time' is not null then
|
|
v_reported := extract(hour from (p_case.baseline_birth_snapshot ->> 'reported_birth_time')::time)::integer * 60
|
|
+ extract(minute from (p_case.baseline_birth_snapshot ->> 'reported_birth_time')::time)::integer;
|
|
v_delta := v_start_clock - v_reported;
|
|
if v_delta > 720 then v_origin := v_origin - interval '1 day';
|
|
elsif v_delta < -720 then v_origin := v_origin + interval '1 day'; end if;
|
|
elsif p_case.baseline_birth_snapshot ->> 'declared_window_end' < p_case.baseline_birth_snapshot ->> 'declared_window_start'
|
|
and p_state ->> 'range_start' < p_case.baseline_birth_snapshot ->> 'declared_window_start' then
|
|
v_origin := v_origin + interval '1 day';
|
|
end if;
|
|
v_end := v_origin + make_interval(mins => (extract(hour from (p_state ->> 'range_end')::time)::integer * 60
|
|
+ extract(minute from (p_state ->> 'range_end')::time)::integer - v_start_clock + 1440) % 1440);
|
|
end if;
|
|
if v_origin is null or v_end is null or extract(epoch from (v_end - v_origin))/60 + 1 <> v_width then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
v_index := 0;
|
|
for v_minute in select value from jsonb_array_elements(v_minutes) loop
|
|
if v_minute -> 'offset' is distinct from to_jsonb(v_index)
|
|
or v_minute ->> 'time' is distinct from to_char(v_origin + make_interval(mins => v_index), 'HH24:MI')
|
|
or v_minute ->> 'date' is distinct from to_char(v_origin + make_interval(mins => v_index), 'YYYY-MM-DD')
|
|
or jsonb_typeof(v_minute -> 'signs') is distinct from 'object'
|
|
or exists (select 1 from jsonb_array_elements_text(v_targets) target
|
|
where coalesce(v_minute -> 'signs' ->> target, '') !~ '^([0-9]|1[01])$') then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
v_index := v_index + 1;
|
|
end loop;
|
|
|
|
-- Raw posterior is server-owned ledger output. Prior and real cluster members
|
|
-- must still identify the persisted engine candidate, never client-supplied mass.
|
|
for v_candidate in select value from jsonb_array_elements(p_state -> 'candidates') loop
|
|
select candidate_payload into v_payload from public.agentic_rectification_candidates
|
|
where result_id = p_result.id and user_id = p_case.user_id and case_id = p_case.id
|
|
and to_char(candidate_time, 'HH24:MI') = v_candidate ->> 'time';
|
|
if not found or jsonb_typeof(v_candidate -> 'raw_posterior_score') is distinct from 'number'
|
|
or jsonb_typeof(v_candidate -> 'raw_eliminated') is distinct from 'boolean'
|
|
or v_candidate -> 'raw_prior_score' is distinct from v_payload -> 'raw_score'
|
|
or jsonb_typeof(v_payload -> 'raw_score') is distinct from 'number'
|
|
or v_candidate -> 'cluster_times' is distinct from v_payload -> 'cluster_times'
|
|
or jsonb_typeof(v_candidate -> 'cluster_times') is distinct from 'array'
|
|
or jsonb_array_length(v_candidate -> 'cluster_times') = 0
|
|
or (select count(distinct value) from jsonb_array_elements(v_candidate -> 'cluster_times')) <> jsonb_array_length(v_candidate -> 'cluster_times')
|
|
or exists (select 1 from jsonb_array_elements_text(v_candidate -> 'cluster_times') member
|
|
where not exists (select 1 from jsonb_array_elements(v_minutes) m where m ->> 'time' = member)) then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
end loop;
|
|
select max((value ->> 'raw_posterior_score')::double precision) into v_peak
|
|
from jsonb_array_elements(p_state -> 'candidates') where value -> 'raw_eliminated' = 'false'::jsonb;
|
|
select coalesce(jsonb_object_agg(clock, mass), '{}'::jsonb) into v_weights from (
|
|
select member as clock, sum(greatest((candidate ->> 'raw_posterior_score')::double precision, 0)
|
|
/ jsonb_array_length(candidate -> 'cluster_times')) as mass
|
|
from jsonb_array_elements(p_state -> 'candidates') candidate,
|
|
lateral jsonb_array_elements_text(candidate -> 'cluster_times') member
|
|
where candidate -> 'raw_eliminated' = 'false'::jsonb
|
|
and v_peak - (candidate ->> 'raw_posterior_score')::double precision < 8
|
|
group by member
|
|
) weights;
|
|
|
|
v_intersection_end := v_width - 1;
|
|
v_index := 0;
|
|
for v_chart in select value from jsonb_array_elements(v_summary -> 'charts') loop
|
|
if v_chart -> 'chart' is distinct from v_targets -> v_index then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
with numbered as (
|
|
select (m ->> 'offset')::integer as minute_offset, (m -> 'signs' ->> (v_chart ->> 'chart'))::integer as sign,
|
|
lag((m -> 'signs' ->> (v_chart ->> 'chart'))::integer) over (order by (m ->> 'offset')::integer) as previous
|
|
from jsonb_array_elements(v_minutes) m
|
|
), runs as (
|
|
select *, sum(case when sign is distinct from previous then 1 else 0 end) over (order by minute_offset) - 1 as segment
|
|
from numbered
|
|
) select jsonb_agg(jsonb_build_object('index', segment, 'start', start, 'end', finish, 'key', jsonb_build_array(sign)) order by segment)
|
|
into v_segments from (select segment, min(minute_offset) as start, max(minute_offset) as finish, min(sign) as sign from runs group by segment) x;
|
|
if v_chart -> 'segments' is distinct from v_segments then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
with mass as (
|
|
select (s ->> 'index')::integer as index, coalesce(sum((v_weights ->> (m ->> 'time'))::double precision), 0) as mass
|
|
from jsonb_array_elements(v_segments) s join jsonb_array_elements(v_minutes) m
|
|
on (m ->> 'offset')::integer between (s ->> 'start')::integer and (s ->> 'end')::integer
|
|
group by s
|
|
), shares as (
|
|
select *, case when sum(mass) over () > 0 then mass / sum(mass) over () else 0 end as share from mass
|
|
) select jsonb_agg(round(share::numeric, 6) order by index),
|
|
count(*) filter (where mass > 1e-9),
|
|
(array_agg(index order by share desc, index) filter (where mass > 1e-9))[1],
|
|
coalesce(max(share) filter (where mass > 1e-9), 0),
|
|
coalesce((array_agg(share order by share desc, index) filter (where mass > 1e-9))[1]
|
|
- (array_agg(share order by share desc, index) filter (where mass > 1e-9))[2] < 1e-9, false)
|
|
into v_shares, v_alive, v_top, v_share, v_tied from shares;
|
|
v_unique := jsonb_array_length(v_segments) = 1;
|
|
v_all_unique := v_all_unique and v_unique;
|
|
v_sign := case when v_top is not null then (v_segments -> v_top -> 'key' ->> 0)::integer
|
|
when v_unique then (v_segments -> 0 -> 'key' ->> 0)::integer else null end;
|
|
v_share := round(v_share::numeric, 6)::double precision;
|
|
v_tier := case when v_chart ->> 'chart' = 'D1' and v_unique then 'certain'
|
|
when v_chart ->> 'chart' <> 'D1' and v_width > 61 then 'blocked'
|
|
when v_share >= 0.6 then 'credible' when v_width <= 21 and v_share >= 0.5 then 'tentative' else 'indistinct' end;
|
|
if v_chart -> 'shares' is distinct from v_shares
|
|
or v_chart -> 'alive_segments' is distinct from to_jsonb(v_alive)
|
|
or v_chart -> 'top_segment' is distinct from coalesce(to_jsonb(v_top), 'null'::jsonb)
|
|
or v_chart -> 'top_share' is distinct from to_jsonb(v_share)
|
|
or v_chart -> 'tied' is distinct from to_jsonb(v_tied)
|
|
or v_chart -> 'full_window_unique' is distinct from to_jsonb(v_unique)
|
|
or v_chart -> 'sign' is distinct from coalesce(to_jsonb(v_sign), 'null'::jsonb)
|
|
or v_chart ->> 'tier' is distinct from v_tier then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
if v_tier <> 'blocked' and v_top is not null then
|
|
v_start := (v_segments -> v_top ->> 'start')::integer;
|
|
v_finish := (v_segments -> v_top ->> 'end')::integer;
|
|
v_intersection_start := greatest(v_intersection_start, v_start);
|
|
v_intersection_end := least(v_intersection_end, v_finish);
|
|
v_current_priority := case v_chart ->> 'chart' when 'D1' then 0 when 'D9' then 1 else 2 end;
|
|
if v_current_priority < v_priority then
|
|
v_priority := v_current_priority; v_fallback_start := v_start; v_fallback_end := v_finish;
|
|
end if;
|
|
end if;
|
|
v_index := v_index + 1;
|
|
end loop;
|
|
if v_summary -> 'no_rectification_needed' is distinct from to_jsonb(v_all_unique) or v_fallback_start is null then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
v_midpoint := case when v_intersection_start <= v_intersection_end
|
|
then (v_intersection_start + v_intersection_end) / 2 else (v_fallback_start + v_fallback_end) / 2 end;
|
|
select m into v_adoption from jsonb_array_elements(v_minutes) m
|
|
where case when jsonb_typeof(p_state -> 'credible_intervals') = 'array' and jsonb_array_length(p_state -> 'credible_intervals') > 0 then
|
|
exists (select 1 from jsonb_array_elements(p_state -> 'credible_intervals') r
|
|
where (m ->> 'date') || 'T' || (m ->> 'time') between r ->> 'start_at' and r ->> 'end_at')
|
|
else case when p_state -> 'credible_range' ->> 0 <= p_state -> 'credible_range' ->> 1
|
|
then m ->> 'time' between p_state -> 'credible_range' ->> 0 and p_state -> 'credible_range' ->> 1
|
|
else m ->> 'time' >= p_state -> 'credible_range' ->> 0 or m ->> 'time' <= p_state -> 'credible_range' ->> 1 end end
|
|
order by abs((m ->> 'offset')::integer - v_midpoint), (m ->> 'offset')::integer limit 1;
|
|
if v_adoption is null or v_summary -> 'adoption_minute' is distinct from v_adoption then
|
|
raise exception 'agentic_rectification_segment_state_inconsistent';
|
|
end if;
|
|
return v_adoption;
|
|
end;
|
|
$$;
|
|
revoke all on function public.rectification_segment_adoption_minute(public.agentic_rectification_cases, public.agentic_rectification_results, jsonb)
|
|
from public, anon, authenticated, service_role;
|
|
|
|
create function public.accept_agentic_rectification_segment_for_case_v1(
|
|
p_user_id uuid, p_case_id uuid, p_result_id uuid, p_candidate_id uuid, p_request_id uuid
|
|
) returns jsonb language plpgsql security definer set search_path = '' as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_result public.agentic_rectification_results%rowtype;
|
|
v_profile public.profiles%rowtype;
|
|
v_existing public.agentic_rectification_candidate_decisions%rowtype;
|
|
v_previous_response jsonb;
|
|
v_state jsonb;
|
|
v_minute jsonb;
|
|
v_response jsonb;
|
|
v_provenance jsonb;
|
|
v_date date;
|
|
v_time time;
|
|
v_offset double precision;
|
|
v_candidate public.agentic_rectification_candidates%rowtype;
|
|
v_candidate_count integer;
|
|
v_candidate_set text;
|
|
v_top_time text;
|
|
v_range_start text;
|
|
v_range_end text;
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_result_id is null or p_candidate_id is null or p_request_id is null then
|
|
raise exception 'agentic_rectification_candidate_invalid_input';
|
|
end if;
|
|
perform pg_advisory_xact_lock(hashtextextended(p_user_id::text || ':' || p_request_id::text, 0));
|
|
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;
|
|
select * into v_result from public.agentic_rectification_results where id = p_result_id and case_id = p_case_id and user_id = p_user_id for update;
|
|
if not found or not exists (select 1 from public.agentic_rectification_candidates
|
|
where id = p_candidate_id and result_id = p_result_id and case_id = p_case_id and user_id = p_user_id) then
|
|
raise exception 'agentic_rectification_candidate_not_found';
|
|
end if;
|
|
select * into v_existing from public.agentic_rectification_candidate_decisions where user_id = p_user_id and request_id = p_request_id for update;
|
|
if found then
|
|
if v_existing.decision_kind <> 'accept' or v_existing.case_id <> p_case_id or v_existing.result_id <> p_result_id or v_existing.candidate_id <> p_candidate_id then
|
|
raise exception 'agentic_rectification_candidate_request_conflict';
|
|
end if;
|
|
-- Old requests remain old receipts; a retry never changes the minute already adopted.
|
|
return jsonb_set(v_existing.response, '{idempotent}', 'true'::jsonb, true);
|
|
end if;
|
|
select inference_state into v_state from public.agentic_rectification_inference_transitions
|
|
where case_id = p_case_id and result_id = p_result_id order by revision desc limit 1;
|
|
v_state := coalesce(v_state, v_result.decision_receipt -> 'inference_state');
|
|
if not (v_state ? 'segment_summary') then
|
|
return public.accept_agentic_rectification_candidate_for_case_v2(p_user_id,p_case_id,p_result_id,p_candidate_id,p_request_id,null);
|
|
end if;
|
|
v_minute := public.rectification_segment_adoption_minute(v_case, v_result, v_state);
|
|
select * into v_profile from public.profiles where id = p_user_id for update;
|
|
if not found then raise exception 'agentic_rectification_candidate_profile_changed'; end if;
|
|
if v_result.selected_candidate_id is not null and v_case.adopted_credible_range ->> 'source' = 'server_segment_midpoint'
|
|
and (v_profile.active_birth_time is distinct from v_result.selected_time
|
|
or v_profile.active_birth_date is distinct from (v_case.adopted_credible_range ->> 'representative_date')::date
|
|
or v_profile.birth_time_status is distinct from 'accepted'
|
|
or v_profile.active_birth_timezone_offset is null
|
|
or v_profile.active_birth_timezone_offset not between -14 and 14
|
|
or v_profile.active_birth_timezone_offset is distinct from (v_profile.active_birth_provenance ->> 'timezone_offset')::double precision
|
|
or v_profile.timezone_id is distinct from v_profile.active_birth_provenance ->> 'timezone_id'
|
|
or v_profile.active_birth_provenance ->> 'contract' is distinct from 'segment-v1'
|
|
or v_profile.active_birth_provenance ->> 'case_id' is distinct from p_case_id::text
|
|
or v_profile.active_birth_provenance ->> 'candidate_id' is distinct from v_result.selected_candidate_id::text
|
|
or v_profile.active_birth_provenance ->> 'candidate_date' is distinct from v_profile.active_birth_date::text
|
|
or v_profile.active_birth_provenance ->> 'candidate_time' is distinct from to_char(v_profile.active_birth_time,'HH24:MI')
|
|
or v_profile.active_birth_provenance ->> 'result_id' is distinct from p_result_id::text) then
|
|
raise exception 'agentic_rectification_candidate_selection_blocked';
|
|
end if;
|
|
-- A new request for the same segment selection must not bounce through the
|
|
-- candidate minute. Revalidate freshness and the frozen dated gate read-only.
|
|
if v_result.selected_candidate_id = p_candidate_id
|
|
and v_case.adopted_credible_range ->> 'source' = 'server_segment_midpoint'
|
|
and v_result.selected_time = (v_minute ->> 'time')::time
|
|
and v_profile.active_birth_date = (v_minute ->> 'date')::date
|
|
and v_case.accepted_time = v_result.selected_time
|
|
and v_result.selection_kind = 'user_accepted'
|
|
and v_case.status = 'candidate_accepted' then
|
|
if v_result.invalidated_at is not null or v_result.expires_at <= now() then
|
|
raise exception 'agentic_rectification_candidate_expired';
|
|
end if;
|
|
if not v_result.display_allowed or not v_result.selection_allowed then
|
|
raise exception 'agentic_rectification_candidate_selection_blocked';
|
|
end if;
|
|
if exists (select 1 from public.agentic_rectification_results newer where newer.user_id=p_user_id
|
|
and newer.case_id=p_case_id and newer.invalidated_at is null and newer.created_at>v_result.created_at) then
|
|
raise exception 'agentic_rectification_candidate_superseded';
|
|
end if;
|
|
if v_profile.birth_date is distinct from (v_case.baseline_birth_snapshot ->> 'birth_date')::date
|
|
or v_profile.reported_birth_time is distinct from (v_case.baseline_birth_snapshot ->> 'reported_birth_time')::time
|
|
or v_profile.birth_time_source is distinct from v_case.baseline_birth_snapshot ->> 'birth_time_source'
|
|
or v_profile.birth_time_period is distinct from v_case.baseline_birth_snapshot ->> 'birth_time_period'
|
|
or v_profile.declared_window_start is distinct from v_case.baseline_birth_snapshot ->> 'declared_window_start'
|
|
or v_profile.declared_window_end is distinct from v_case.baseline_birth_snapshot ->> 'declared_window_end'
|
|
or v_profile.uncertainty_before_minutes is distinct from (v_case.baseline_birth_snapshot ->> 'uncertainty_before_minutes')::integer
|
|
or v_profile.uncertainty_after_minutes is distinct from (v_case.baseline_birth_snapshot ->> 'uncertainty_after_minutes')::integer
|
|
or v_profile.latitude is distinct from (v_case.baseline_birth_snapshot ->> 'latitude')::double precision
|
|
or v_profile.longitude is distinct from (v_case.baseline_birth_snapshot ->> 'longitude')::double precision
|
|
or v_profile.timezone_id is distinct from v_case.baseline_birth_snapshot ->> 'timezone_id'
|
|
or v_profile.timezone_offset is distinct from (v_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision then
|
|
raise exception 'agentic_rectification_candidate_profile_changed';
|
|
end if;
|
|
if jsonb_typeof(v_state -> 'revision') is distinct from 'number' or coalesce(v_state ->> 'revision','') !~ '^[0-9]+$'
|
|
or length(btrim(coalesce(v_state ->> 'candidate_set_id',''))) = 0
|
|
or v_state ->> 'range_start' is distinct from v_case.candidate_range ->> 'start_time'
|
|
or v_state ->> 'range_end' is distinct from v_case.candidate_range ->> 'end_time'
|
|
or exists (select 1 from public.agentic_rectification_inference_transitions t where t.case_id=p_case_id and t.result_id=p_result_id
|
|
and t.revision=(select max(revision) from public.agentic_rectification_inference_transitions where case_id=p_case_id and result_id=p_result_id)
|
|
and (t.revision is distinct from (v_state ->> 'revision')::integer or t.candidate_set_id is distinct from v_state ->> 'candidate_set_id')) then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
select * into v_candidate from public.agentic_rectification_candidates where id=p_candidate_id;
|
|
if public.validate_dated_rectification_candidate(v_case,v_result,v_candidate,v_state) is null then
|
|
-- Frozen legacy identity/active/credible gate, evaluated without any writes.
|
|
select count(*), (v_case.candidate_range ->> 'start_time') || '-' || (v_case.candidate_range ->> 'end_time') || ':' ||
|
|
string_agg(to_char(candidate_time,'HH24:MI'),',' order by candidate_time)
|
|
into v_candidate_count,v_candidate_set from public.agentic_rectification_candidates
|
|
where result_id=p_result_id and case_id=p_case_id and user_id=p_user_id;
|
|
if v_candidate_count=0 or jsonb_array_length(v_state -> 'candidates')<>v_candidate_count
|
|
or v_state ->> 'candidate_set_id' is distinct from v_candidate_set
|
|
or exists (select 1 from jsonb_array_elements(v_state -> 'candidates') a where
|
|
jsonb_typeof(a) is distinct from 'object' or coalesce(a ->> 'time','') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
|
or coalesce(a ->> 'status','') not in ('active','equivalent','winner','eliminated')
|
|
or jsonb_typeof(a -> 'probability') is distinct from 'number' or jsonb_typeof(a -> 'posterior_score') is distinct from 'number'
|
|
or jsonb_typeof(a -> 'cluster_range') is distinct from 'array') then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
if exists (select 1 from jsonb_array_elements(v_state -> 'candidates') a where
|
|
jsonb_array_length(a -> 'cluster_range') is distinct from 2
|
|
or coalesce(a -> 'cluster_range' ->> 0,'') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
|
or coalesce(a -> 'cluster_range' ->> 1,'') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
|
or a -> 'cluster_range' ->> 0 > a ->> 'time' or a -> 'cluster_range' ->> 1 < a ->> 'time'
|
|
or not exists (select 1 from public.agentic_rectification_candidates c where c.result_id=p_result_id
|
|
and to_char(c.candidate_time,'HH24:MI')=a ->> 'time'))
|
|
or (select count(distinct a ->> 'time') from jsonb_array_elements(v_state -> 'candidates') a)<>v_candidate_count then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
select a ->> 'time' into v_top_time from jsonb_array_elements(v_state -> 'candidates') a where a ->> 'status'<>'eliminated'
|
|
order by (a ->> 'probability')::numeric desc,(a ->> 'posterior_score')::numeric desc,a ->> 'time' limit 1;
|
|
select min(a -> 'cluster_range' ->> 0),max(a -> 'cluster_range' ->> 1) into v_range_start,v_range_end
|
|
from jsonb_array_elements(v_state -> 'candidates') a where a ->> 'status'<>'eliminated';
|
|
if v_top_time is null or v_state ->> 'representative_time' is distinct from v_top_time
|
|
or jsonb_typeof(v_state -> 'credible_range') is distinct from 'array' or jsonb_array_length(v_state -> 'credible_range')<>2
|
|
or v_state -> 'credible_range' ->> 0 is distinct from v_range_start or v_state -> 'credible_range' ->> 1 is distinct from v_range_end
|
|
or not exists (select 1 from jsonb_array_elements(v_state -> 'candidates') a
|
|
where a ->> 'time'=to_char(v_candidate.candidate_time,'HH24:MI') and a ->> 'status'<>'eliminated') then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
end if;
|
|
select response into v_previous_response from public.agentic_rectification_candidate_decisions
|
|
where user_id=p_user_id and case_id=p_case_id and result_id=p_result_id and candidate_id=p_candidate_id
|
|
and decision_kind='accept' and response ->> 'adoption_contract'='segment-v1'
|
|
and response ->> 'saved_time'=v_minute ->> 'time' and response ->> 'saved_date'=v_minute ->> 'date'
|
|
order by created_at desc limit 1;
|
|
if v_previous_response is not null then
|
|
v_response := v_previous_response || jsonb_build_object('idempotent',true);
|
|
insert into public.agentic_rectification_candidate_decisions(user_id,case_id,result_id,candidate_id,request_id,decision_kind,response)
|
|
values(p_user_id,p_case_id,p_result_id,p_candidate_id,p_request_id,'accept',v_response);
|
|
return v_response;
|
|
end if;
|
|
end if;
|
|
-- Reuse every frozen accept gate in the same transaction for a new selection.
|
|
-- Any later exception rolls back its writes as well as ours.
|
|
v_response := public.accept_agentic_rectification_candidate_for_case_v2(p_user_id,p_case_id,p_result_id,p_candidate_id,p_request_id,null);
|
|
v_date := (v_minute ->> 'date')::date;
|
|
v_time := (v_minute ->> 'time')::time;
|
|
v_offset := coalesce((v_result.decision_receipt ->> 'candidate_timezone_offset')::double precision, v_profile.timezone_offset);
|
|
v_provenance := jsonb_build_object('contract','segment-v1','case_id',p_case_id,'result_id',p_result_id,'candidate_id',p_candidate_id,
|
|
'declared_birth_date',v_profile.birth_date,'candidate_date',v_date,'candidate_time',v_minute ->> 'time',
|
|
'timezone_offset',v_offset,'timezone_id',v_profile.timezone_id,'source','server_segment_midpoint',
|
|
'window_offset_minutes',v_minute -> 'offset','adopted_at',now());
|
|
update public.profiles set birth_time=v_time, active_birth_time=v_time, active_birth_date=v_date,
|
|
active_birth_timezone_offset=v_offset, active_birth_provenance=v_provenance, updated_at=now() where id=p_user_id;
|
|
update public.agentic_rectification_results set selected_time=v_time, updated_at=now() where id=p_result_id;
|
|
update public.agentic_rectification_cases set accepted_time=v_time,
|
|
adopted_credible_range=coalesce(adopted_credible_range,'{}'::jsonb) || jsonb_build_object('representative_time',v_minute ->> 'time',
|
|
'representative_date',v_date,'source','server_segment_midpoint','segment_summary',v_state -> 'segment_summary'),
|
|
updated_at=now() where id=p_case_id;
|
|
v_response := v_response || jsonb_build_object('saved_time',v_minute ->> 'time','saved_date',v_date,
|
|
'day_offset',v_date-v_profile.birth_date,'date_changed',v_date<>v_profile.birth_date,
|
|
'saved_timezone_offset',v_offset,'adoption_contract','segment-v1');
|
|
update public.agentic_rectification_candidate_decisions set response=v_response where user_id=p_user_id and request_id=p_request_id;
|
|
return v_response;
|
|
end;
|
|
$$;
|
|
revoke all on function public.accept_agentic_rectification_segment_for_case_v1(uuid,uuid,uuid,uuid,uuid) from public, anon, authenticated;
|
|
grant execute on function public.accept_agentic_rectification_segment_for_case_v1(uuid,uuid,uuid,uuid,uuid) to service_role;
|
|
|
|
-- Expose only the adopted civil identity, never the profile or its full provenance.
|
|
create function public.get_agentic_rectification_segment_selection_v1(p_user_id uuid, p_case_id uuid, p_result_id uuid)
|
|
returns jsonb language sql security definer set search_path = '' as $$
|
|
select jsonb_build_object('result_id', r.id, 'candidate_id', r.selected_candidate_id,
|
|
'date', p.active_birth_date, 'time', to_char(p.active_birth_time, 'HH24:MI'), 'contract', 'segment-v1')
|
|
from public.agentic_rectification_results r
|
|
join public.agentic_rectification_cases c on c.id=r.case_id and c.user_id=r.user_id
|
|
join public.profiles p on p.id=r.user_id
|
|
where r.id=p_result_id and r.case_id=p_case_id and r.user_id=p_user_id
|
|
and r.selection_kind='user_accepted' and p.birth_time_status='accepted' and c.status='candidate_accepted'
|
|
and r.invalidated_at is null and r.expires_at>now()
|
|
and not exists (select 1 from public.agentic_rectification_results newer where newer.case_id=c.id and newer.user_id=p_user_id
|
|
and newer.invalidated_at is null and newer.created_at>r.created_at)
|
|
and p.active_birth_timezone_offset is not null and p.active_birth_timezone_offset between -14 and 14
|
|
and p.active_birth_timezone_offset=(p.active_birth_provenance ->> 'timezone_offset')::double precision
|
|
and p.timezone_id is not distinct from p.active_birth_provenance ->> 'timezone_id'
|
|
and c.accepted_time=r.selected_time and p.active_birth_time=r.selected_time
|
|
and c.adopted_credible_range ->> 'source'='server_segment_midpoint'
|
|
and c.adopted_credible_range ->> 'representative_date'=p.active_birth_date::text
|
|
and p.active_birth_provenance ->> 'contract'='segment-v1'
|
|
and p.active_birth_provenance ->> 'case_id'=c.id::text
|
|
and p.active_birth_provenance ->> 'result_id'=r.id::text
|
|
and p.active_birth_provenance ->> 'candidate_id'=r.selected_candidate_id::text
|
|
and p.active_birth_provenance ->> 'candidate_date'=p.active_birth_date::text
|
|
and p.active_birth_provenance ->> 'candidate_time'=to_char(p.active_birth_time,'HH24:MI');
|
|
$$;
|
|
revoke all on function public.get_agentic_rectification_segment_selection_v1(uuid,uuid,uuid) from public, anon, authenticated;
|
|
grant execute on function public.get_agentic_rectification_segment_selection_v1(uuid,uuid,uuid) to service_role;
|
|
commit;
|