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>
470 lines
19 KiB
PL/PgSQL
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;
|