Files
Jyotisha/frontend/supabase/migrations/20260906020000_adopted_credible_range.sql
T
Jesse_ChenandCursor 814c924e4a
Independent Staging Quality Gate / validate (push) Successful in 9m20s
Independent Staging Quality Gate / publish (push) Successful in 6m51s
fix(rectification): exhaustion exit, explain layer, range reading, unknown-time scan (BUG-565–568)
Keep askable cards after exhaustion, explain each probe, read the adopted credible range in reports and chat, and compare declared periods before the minute grid when the clock is unknown.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 09:10:37 +08:00

470 lines
19 KiB
PL/PgSQL

-- Adopted credible range is the interview window written at accept.
-- Opening candidate_range stays the search baseline and is never rewritten here.
-- read_report_candidate_range prefers adopted_credible_range, then candidate_range.
begin;
do $migration$
begin
if current_user <> 'schema_owner' then
raise exception 'adopted_credible_range_requires_schema_owner'
using errcode = '42501';
end if;
end
$migration$;
alter table public.agentic_rectification_cases
add column if not exists adopted_credible_range jsonb;
comment on column public.agentic_rectification_cases.adopted_credible_range is
'Interview credible range written at accept; never overwrites candidate_range.';
drop function if exists public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid);
create or replace function public.accept_agentic_rectification_candidate_for_case_v2(
p_user_id uuid,
p_case_id uuid,
p_result_id uuid,
p_candidate_id uuid,
p_request_id uuid,
p_credible_range jsonb default null
)
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_candidate public.agentic_rectification_candidates%rowtype;
v_profile public.profiles%rowtype;
v_snapshot jsonb;
v_inference jsonb;
v_expected_candidate_set_id text;
v_persisted_candidate_count integer;
v_top_active_time text;
v_active_range_start text;
v_active_range_end text;
v_latest_transition public.agentic_rectification_inference_transitions%rowtype;
v_existing_decision public.agentic_rectification_candidate_decisions%rowtype;
v_response jsonb;
v_adopted jsonb;
v_adopted_start text;
v_adopted_end text;
v_adopted_rep text;
v_adopted_width integer;
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' using errcode = 'P0001';
end if;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.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' using errcode = 'P0001';
end if;
select * into v_result
from public.agentic_rectification_results
where id = p_result_id
and user_id = p_user_id
and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001';
end if;
select * into v_candidate
from public.agentic_rectification_candidates
where id = p_candidate_id
and result_id = p_result_id
and user_id = p_user_id
and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001';
end if;
select * into v_existing_decision
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.decision_kind <> 'accept'
or v_existing_decision.case_id <> p_case_id
or v_existing_decision.result_id <> p_result_id
or v_existing_decision.candidate_id <> p_candidate_id then
raise exception 'agentic_rectification_candidate_request_conflict' using errcode = 'P0001';
end if;
return jsonb_set(v_existing_decision.response, '{idempotent}', 'true'::jsonb, true);
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
if v_result.invalidated_at is not null or v_result.expires_at <= pg_catalog.now() then
raise exception 'agentic_rectification_candidate_expired' using errcode = 'P0001';
end if;
if not v_result.display_allowed or not v_result.selection_allowed then
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
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' using errcode = 'P0001';
end if;
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' using errcode = 'P0001';
end if;
v_snapshot := v_case.baseline_birth_snapshot;
if v_profile.birth_date is distinct from (v_snapshot ->> 'birth_date')::date
or v_profile.reported_birth_time is distinct from (v_snapshot ->> 'reported_birth_time')::time without time zone
or v_profile.birth_time_source is distinct from v_snapshot ->> 'birth_time_source'
or v_profile.birth_time_period is distinct from v_snapshot ->> 'birth_time_period'
or v_profile.declared_window_start is distinct from v_snapshot ->> 'declared_window_start'
or v_profile.declared_window_end is distinct from v_snapshot ->> 'declared_window_end'
or v_profile.uncertainty_before_minutes is distinct from (v_snapshot ->> 'uncertainty_before_minutes')::integer
or v_profile.uncertainty_after_minutes is distinct from (v_snapshot ->> 'uncertainty_after_minutes')::integer
or v_profile.latitude is distinct from (v_snapshot ->> 'latitude')::double precision
or v_profile.longitude is distinct from (v_snapshot ->> 'longitude')::double precision
or v_profile.timezone_id is distinct from v_snapshot ->> 'timezone_id'
or v_profile.timezone_offset is distinct from (v_snapshot ->> 'timezone_offset')::double precision then
raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001';
end if;
-- Adoption is a trust-boundary write. Structured answers append inference
-- transitions instead of mutating the engine result, so the latest transition
-- is authoritative when present; persisted display candidates remain a cache.
select * into v_latest_transition
from public.agentic_rectification_inference_transitions
where case_id = p_case_id
and result_id = v_result.id
order by revision desc
limit 1;
v_inference := case
when v_latest_transition.id is not null then v_latest_transition.inference_state
else v_result.decision_receipt -> 'inference_state'
end;
if v_inference is null
or jsonb_typeof(v_inference) <> 'object'
or jsonb_typeof(v_inference -> 'candidates') <> 'array'
or jsonb_array_length(v_inference -> 'candidates') = 0
or jsonb_typeof(v_inference -> 'revision') is distinct from 'number'
or coalesce((v_inference ->> 'revision') !~ '^[0-9]+$', true)
or length(btrim(coalesce(v_inference ->> 'candidate_set_id', ''))) = 0
or (v_inference ->> 'range_start') is distinct from v_case.candidate_range ->> 'start_time'
or (v_inference ->> 'range_end') is distinct from v_case.candidate_range ->> 'end_time' then
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
end if;
if v_latest_transition.id is not null
and (
v_latest_transition.revision is distinct from (v_inference ->> 'revision')::integer
or v_latest_transition.candidate_set_id is distinct from v_inference ->> 'candidate_set_id'
) then
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
end if;
select count(*),
(v_case.candidate_range ->> 'start_time') || '-' ||
(v_case.candidate_range ->> 'end_time') || ':' ||
string_agg(pg_catalog.to_char(candidate_time, 'HH24:MI'), ',' order by candidate_time)
into v_persisted_candidate_count, v_expected_candidate_set_id
from public.agentic_rectification_candidates
where result_id = v_result.id
and user_id = p_user_id
and case_id = p_case_id;
if v_persisted_candidate_count = 0
or jsonb_array_length(v_inference -> 'candidates') <> v_persisted_candidate_count
or (v_inference ->> 'candidate_set_id') is distinct from v_expected_candidate_set_id
or exists (
select 1
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
where jsonb_typeof(item.value) <> 'object'
) then
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
end if;
if exists (
select 1
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
where coalesce((item.value ->> 'time') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true)
or coalesce(item.value ->> 'status', '') not in ('active', 'equivalent', 'winner', 'eliminated')
or jsonb_typeof(item.value -> 'probability') is distinct from 'number'
or jsonb_typeof(item.value -> 'posterior_score') is distinct from 'number'
or jsonb_typeof(item.value -> 'cluster_range') is distinct from 'array'
) then
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
end if;
if exists (
select 1
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
where jsonb_array_length(item.value -> 'cluster_range') is distinct from 2
or coalesce((item.value -> 'cluster_range' ->> 0) !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true)
or coalesce((item.value -> 'cluster_range' ->> 1) !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true)
or item.value -> 'cluster_range' ->> 0 > item.value ->> 'time'
or item.value -> 'cluster_range' ->> 1 < item.value ->> 'time'
or not exists (
select 1
from public.agentic_rectification_candidates persisted
where persisted.result_id = v_result.id
and pg_catalog.to_char(persisted.candidate_time, 'HH24:MI') = item.value ->> 'time'
)
) or (
select count(distinct item.value ->> 'time')
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
) <> v_persisted_candidate_count then
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
end if;
select item.value ->> 'time'
into v_top_active_time
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
where item.value ->> 'status' <> 'eliminated'
order by (item.value ->> 'probability')::numeric desc,
(item.value ->> 'posterior_score')::numeric desc,
item.value ->> 'time'
limit 1;
select min(item.value -> 'cluster_range' ->> 0),
max(item.value -> 'cluster_range' ->> 1)
into v_active_range_start, v_active_range_end
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
where item.value ->> 'status' <> 'eliminated';
if v_top_active_time is null
or (v_inference ->> 'representative_time') is distinct from v_top_active_time
or jsonb_typeof(v_inference -> 'credible_range') <> 'array'
or jsonb_array_length(v_inference -> 'credible_range') <> 2
or (v_inference -> 'credible_range' ->> 0) is distinct from v_active_range_start
or (v_inference -> 'credible_range' ->> 1) is distinct from v_active_range_end
or not exists (
select 1
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
where item.value ->> 'time' = pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI')
and item.value ->> 'status' <> 'eliminated'
) then
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
end if;
if v_result.selected_candidate_id is not null then
if v_result.selected_candidate_id = p_candidate_id
and v_result.selected_time is not distinct from v_candidate.candidate_time
and v_case.accepted_time is not distinct from v_candidate.candidate_time
and v_result.selection_kind is not distinct from 'user_accepted'
and v_profile.active_birth_time is not distinct from v_candidate.candidate_time
and v_profile.birth_time_status is not distinct from 'accepted' then
v_response := jsonb_build_object(
'success', true,
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
'status', 'accepted',
'result_id', v_result.id,
'candidate_id', v_candidate.id,
'case_status', 'candidate_accepted',
'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;
if v_case.status is distinct from 'candidate_accepted'
or v_result.selection_kind is distinct from 'user_accepted'
or v_profile.active_birth_time is distinct from v_result.selected_time
or v_profile.birth_time_status is distinct from 'accepted' then
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
end if;
else
if v_result.selected_time is not null then
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
end if;
end if;
update public.profiles
set active_birth_time = v_candidate.candidate_time,
birth_time = v_candidate.candidate_time,
birth_time_status = 'accepted',
rectification_confidence = case
when v_result.overall_confidence = 'high' then 100
when v_result.overall_confidence = 'medium' then 70
else 40
end,
updated_at = pg_catalog.now()
where id = p_user_id;
update public.agentic_rectification_results
set selected_candidate_id = p_candidate_id,
selected_time = v_candidate.candidate_time,
selection_kind = 'user_accepted',
selected_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = v_result.id;
update public.agentic_rectification_results
set invalidated_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where user_id = p_user_id
and case_id = p_case_id
and id <> v_result.id
and invalidated_at is null
and selected_time is null;
v_adopted := null;
if p_credible_range is not null
and jsonb_typeof(p_credible_range) = 'object' then
v_adopted_start := nullif(btrim(coalesce(p_credible_range->>'start_time', '')), '');
v_adopted_end := nullif(btrim(coalesce(p_credible_range->>'end_time', '')), '');
v_adopted_rep := nullif(btrim(coalesce(p_credible_range->>'representative_time', '')), '');
begin
v_adopted_width := (p_credible_range->>'width_minutes')::integer;
exception when others then
v_adopted_width := null;
end;
end if;
if v_adopted_start is null or v_adopted_end is null
or v_adopted_start !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
or v_adopted_end !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
v_adopted_start := v_inference -> 'credible_range' ->> 0;
v_adopted_end := v_inference -> 'credible_range' ->> 1;
v_adopted_rep := v_inference ->> 'representative_time';
v_adopted_width := null;
end if;
if v_adopted_start is not null
and v_adopted_end is not null
and v_adopted_start ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
and v_adopted_end ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
v_adopted := jsonb_strip_nulls(jsonb_build_object(
'start_time', v_adopted_start,
'end_time', v_adopted_end,
'representative_time', coalesce(v_adopted_rep, v_top_active_time),
'width_minutes', v_adopted_width,
'source', 'inference_credible_range'
));
end if;
update public.agentic_rectification_cases
set status = 'candidate_accepted',
accepted_time = v_candidate.candidate_time,
confirmed_time = null,
completed_at = null,
adopted_credible_range = coalesce(v_adopted, adopted_credible_range),
last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = v_case.id;
v_response := jsonb_build_object(
'success', true,
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
'status', 'accepted',
'result_id', v_result.id,
'candidate_id', v_candidate.id,
'case_status', 'candidate_accepted',
'idempotent', false
);
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;
$$;
revoke all on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, jsonb)
from public, anon, authenticated;
grant execute on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, jsonb)
to service_role;
create or replace function public.read_report_candidate_range(
p_user_id uuid,
p_rectification_case_id uuid default null
)
returns jsonb
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
v_start text;
v_end text;
v_range jsonb;
begin
if p_user_id is null then
return null;
end if;
if p_rectification_case_id is not null then
select to_char(c.candidate_start, 'HH24:MI'), to_char(c.candidate_end, 'HH24:MI')
into v_start, v_end
from public.birth_time_rectification_cases as c
where c.id = p_rectification_case_id
and c.user_id = p_user_id
and c.status in ('confirmed', 'completed')
and c.candidate_start is not null
and c.candidate_end is not null;
if found then
return jsonb_build_object('start_time', v_start, 'end_time', v_end);
end if;
end if;
select case
when c.adopted_credible_range is not null
and nullif(btrim(coalesce(c.adopted_credible_range->>'start_time', '')), '') is not null
and nullif(btrim(coalesce(c.adopted_credible_range->>'end_time', '')), '') is not null
then c.adopted_credible_range
else c.candidate_range
end
into v_range
from public.agentic_rectification_cases as c
where c.user_id = p_user_id
and c.status = 'candidate_accepted'
order by c.updated_at desc
limit 1;
if v_range is null then
return null;
end if;
v_start := nullif(btrim(coalesce(v_range->>'start_time', '')), '');
v_end := nullif(btrim(coalesce(v_range->>'end_time', '')), '');
if v_start is null or v_end is null then
return null;
end if;
return jsonb_build_object('start_time', v_start, 'end_time', v_end);
end;
$$;
revoke all on function public.read_report_candidate_range(uuid, uuid)
from public, anon, authenticated;
grant execute on function public.read_report_candidate_range(uuid, uuid)
to service_role;
commit;