Files
Jyotisha/frontend/supabase/migrations/20260907020000_rectification_declared_uncertainty.sql
T
Jesse_Chen 8e31680b45 fix(rectification): honor declared birth-time uncertainty and split windows over two hours (BUG-571–573)
Intake stores how sure the user is; rectification now searches that range, offers a one-click widen when event fit is low at the edge, and trisects windows longer than two hours before the minute grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 12:18:50 +08:00

701 lines
24 KiB
PL/PgSQL

-- Declared uncertainty: sub-window block_scan, widen RPC, date reliability.
-- Exclusive span > 120 stays in block_scan; inclusive width cap for widen is 241.
begin;
do $migration$
begin
if current_user <> 'schema_owner' then
raise exception 'rectification_declared_uncertainty_requires_schema_owner'
using errcode = '42501';
end if;
end
$migration$;
alter table public.agentic_rectification_cases
add column if not exists widen_declined_at_fingerprint text;
comment on column public.agentic_rectification_cases.widen_declined_at_fingerprint is
'Evidence ledger fingerprint at which the user declined a widen-window card.';
alter table public.agentic_rectification_evidence
add column if not exists date_reliability text;
alter table public.agentic_rectification_evidence
drop constraint if exists agentic_rectification_evidence_date_reliability_check;
alter table public.agentic_rectification_evidence
add constraint agentic_rectification_evidence_date_reliability_check
check (
date_reliability is null
or date_reliability in ('high', 'medium', 'low', 'uncertain', 'unreliable')
);
comment on column public.agentic_rectification_evidence.date_reliability is
'Optional day-event reliability: high = checked record, medium = memory.';
create or replace function public.agentic_rectification_is_clock(p_clock text)
returns boolean
language sql
immutable
parallel safe
set search_path = ''
as $$
select p_clock is not null
and p_clock ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$';
$$;
create or replace function public.agentic_rectification_clock_minutes(p_clock text)
returns integer
language sql
immutable
strict
parallel safe
set search_path = ''
as $$
select split_part(p_clock, ':', 1)::integer * 60 + split_part(p_clock, ':', 2)::integer;
$$;
create or replace function public.agentic_rectification_clock_span_minutes(
p_start text,
p_end text
)
returns integer
language sql
immutable
strict
parallel safe
set search_path = ''
as $$
select case
when public.agentic_rectification_clock_minutes(p_end)
>= public.agentic_rectification_clock_minutes(p_start)
then public.agentic_rectification_clock_minutes(p_end)
- public.agentic_rectification_clock_minutes(p_start)
else 1440 - public.agentic_rectification_clock_minutes(p_start)
+ public.agentic_rectification_clock_minutes(p_end)
end;
$$;
create or replace function public.agentic_rectification_clock_inclusive_width(
p_start text,
p_end text
)
returns integer
language sql
immutable
strict
parallel safe
set search_path = ''
as $$
select public.agentic_rectification_clock_span_minutes(p_start, p_end) + 1;
$$;
create or replace function public.agentic_rectification_clock_contains(
p_outer_start text,
p_outer_end text,
p_inner_start text,
p_inner_end text
)
returns boolean
language plpgsql
immutable
strict
parallel safe
set search_path = ''
as $$
declare
v_outer_width integer;
v_origin integer;
v_inner_start integer;
v_inner_end integer;
v_outer_last integer;
begin
v_outer_width := public.agentic_rectification_clock_inclusive_width(p_outer_start, p_outer_end);
if v_outer_width >= 1440 then
return true;
end if;
v_origin := public.agentic_rectification_clock_minutes(p_outer_start);
v_inner_start := (
public.agentic_rectification_clock_minutes(p_inner_start) - v_origin + 1440
) % 1440;
v_inner_end := (
public.agentic_rectification_clock_minutes(p_inner_end) - v_origin + 1440
) % 1440;
v_outer_last := v_outer_width - 1;
if v_inner_start <= v_inner_end then
return v_inner_start <= v_outer_last and v_inner_end <= v_outer_last;
end if;
return false;
end;
$$;
revoke all on function public.agentic_rectification_is_clock(text)
from public, anon, authenticated;
revoke all on function public.agentic_rectification_clock_minutes(text)
from public, anon, authenticated;
revoke all on function public.agentic_rectification_clock_span_minutes(text, text)
from public, anon, authenticated;
revoke all on function public.agentic_rectification_clock_inclusive_width(text, text)
from public, anon, authenticated;
revoke all on function public.agentic_rectification_clock_contains(text, text, text, text)
from public, anon, authenticated;
grant execute on function public.agentic_rectification_is_clock(text) to service_role;
grant execute on function public.agentic_rectification_clock_minutes(text) to service_role;
grant execute on function public.agentic_rectification_clock_span_minutes(text, text) to service_role;
grant execute on function public.agentic_rectification_clock_inclusive_width(text, text) to service_role;
grant execute on function public.agentic_rectification_clock_contains(text, text, text, text) to service_role;
create or replace function public.set_agentic_rectification_case_stage(
p_user_id uuid,
p_case_id uuid,
p_stage text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
begin
if p_user_id is null or p_case_id is null or p_stage not in ('minute', 'block_scan') then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
update public.agentic_rectification_cases
set
stage = p_stage,
last_activity_at = pg_catalog.now()
where id = v_case.id;
return jsonb_build_object(
'case_id', v_case.id,
'stage', p_stage
);
end;
$$;
create or replace function public.advance_agentic_rectification_case_from_block_scan(
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
v_case public.agentic_rectification_cases%rowtype;
v_range jsonb;
v_current_start text;
v_current_end text;
v_span integer;
v_rounds integer;
v_stage text;
v_block_scan jsonb;
begin
if p_user_id is null or p_case_id is null
or not public.agentic_rectification_is_clock(p_start_time)
or not public.agentic_rectification_is_clock(p_end_time)
or p_start_time = p_end_time then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
if v_case.stage is distinct from 'block_scan' then
raise exception 'agentic_rectification_not_block_scan' using errcode = 'P0001';
end if;
v_current_start := coalesce(v_case.candidate_range->>'start_time', '');
v_current_end := coalesce(v_case.candidate_range->>'end_time', '');
if not public.agentic_rectification_is_clock(v_current_start)
or not public.agentic_rectification_is_clock(v_current_end) then
raise exception 'agentic_rectification_invalid_range' using errcode = 'P0001';
end if;
if not public.agentic_rectification_clock_contains(
v_current_start, v_current_end, p_start_time, p_end_time
) then
raise exception 'agentic_rectification_invalid_block_window' using errcode = 'P0001';
end if;
v_span := public.agentic_rectification_clock_span_minutes(p_start_time, p_end_time);
begin
v_rounds := coalesce((v_case.block_scan->>'rounds')::integer, 0);
exception
when invalid_text_representation then
v_rounds := 0;
end;
if v_rounds < 0 then
v_rounds := 0;
end if;
v_range := jsonb_build_object('start_time', p_start_time, 'end_time', p_end_time);
if v_span > 120 and v_rounds < 3 then
v_stage := 'block_scan';
v_block_scan := jsonb_build_object('rounds', v_rounds);
else
v_stage := 'minute';
v_block_scan := null;
end if;
update public.agentic_rectification_results
set invalidated_at = pg_catalog.now()
where case_id = v_case.id
and invalidated_at is null;
update public.agentic_rectification_cases
set
candidate_range = v_range,
stage = v_stage,
block_scan = v_block_scan,
last_activity_at = pg_catalog.now()
where id = v_case.id;
return jsonb_build_object(
'case_id', v_case.id,
'stage', v_stage,
'candidate_range', v_range
);
end;
$$;
create or replace function public.widen_agentic_rectification_case_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
v_case public.agentic_rectification_cases%rowtype;
v_range jsonb;
v_current_start text;
v_current_end text;
v_old_width integer;
v_new_width integer;
begin
if p_user_id is null or p_case_id is null
or not public.agentic_rectification_is_clock(p_start_time)
or not public.agentic_rectification_is_clock(p_end_time)
or p_start_time = p_end_time then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
if v_case.accepted_time is not null then
raise exception 'agentic_rectification_already_adopted' using errcode = 'P0001';
end if;
if coalesce(v_case.stage, 'minute') is distinct from 'minute' then
raise exception 'agentic_rectification_not_minute' using errcode = 'P0001';
end if;
v_current_start := coalesce(v_case.candidate_range->>'start_time', '');
v_current_end := coalesce(v_case.candidate_range->>'end_time', '');
if not public.agentic_rectification_is_clock(v_current_start)
or not public.agentic_rectification_is_clock(v_current_end) then
raise exception 'agentic_rectification_invalid_range' using errcode = 'P0001';
end if;
if not public.agentic_rectification_clock_contains(
p_start_time, p_end_time, v_current_start, v_current_end
) then
raise exception 'agentic_rectification_invalid_widen_window' using errcode = 'P0001';
end if;
v_old_width := public.agentic_rectification_clock_inclusive_width(v_current_start, v_current_end);
v_new_width := public.agentic_rectification_clock_inclusive_width(p_start_time, p_end_time);
if v_new_width <= v_old_width or v_new_width > 241 then
raise exception 'agentic_rectification_invalid_widen_window' using errcode = 'P0001';
end if;
v_range := jsonb_build_object('start_time', p_start_time, 'end_time', p_end_time);
update public.agentic_rectification_results
set invalidated_at = pg_catalog.now()
where case_id = v_case.id
and invalidated_at is null;
update public.agentic_rectification_cases
set
candidate_range = v_range,
last_activity_at = pg_catalog.now()
where id = v_case.id;
return jsonb_build_object(
'case_id', v_case.id,
'stage', 'minute',
'candidate_range', v_range
);
end;
$$;
create or replace function public.set_agentic_rectification_widen_declined(
p_user_id uuid,
p_case_id uuid,
p_fingerprint text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
begin
if p_user_id is null or p_case_id is null
or p_fingerprint is null or btrim(p_fingerprint) = '' then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
update public.agentic_rectification_cases
set
widen_declined_at_fingerprint = btrim(p_fingerprint),
last_activity_at = pg_catalog.now()
where id = v_case.id;
return jsonb_build_object(
'case_id', v_case.id,
'widen_declined_at_fingerprint', btrim(p_fingerprint)
);
end;
$$;
create or replace function public.set_agentic_rectification_evidence_date_reliability(
p_user_id uuid,
p_case_id uuid,
p_evidence_id uuid,
p_date_reliability text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_evidence public.agentic_rectification_evidence%rowtype;
begin
if p_user_id is null or p_case_id is null or p_evidence_id is null
or p_date_reliability not in ('high', 'medium') then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
select * into v_evidence
from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = v_case.id
for update;
if not found then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
update public.agentic_rectification_evidence
set date_reliability = p_date_reliability
where id = v_evidence.id;
return jsonb_build_object(
'evidence_id', v_evidence.id,
'date_reliability', p_date_reliability
);
end;
$$;
revoke all on function public.widen_agentic_rectification_case_window(uuid, uuid, text, text)
from public, anon, authenticated;
grant execute on function public.widen_agentic_rectification_case_window(uuid, uuid, text, text)
to service_role;
revoke all on function public.set_agentic_rectification_widen_declined(uuid, uuid, text)
from public, anon, authenticated;
grant execute on function public.set_agentic_rectification_widen_declined(uuid, uuid, text)
to service_role;
revoke all on function public.set_agentic_rectification_evidence_date_reliability(uuid, uuid, uuid, text)
from public, anon, authenticated;
grant execute on function public.set_agentic_rectification_evidence_date_reliability(uuid, uuid, uuid, text)
to service_role;
create or replace function public.get_agentic_rectification_case(
p_user_id uuid,
p_case_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_evidence_count bigint;
v_turn_count bigint;
begin
if p_user_id is null or p_case_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select count(*) into v_evidence_count
from public.agentic_rectification_evidence
where case_id = v_case.id;
select count(*) into v_turn_count
from public.agentic_rectification_turns
where case_id = v_case.id;
select * into v_result
from public.agentic_rectification_results
where case_id = v_case.id
and invalidated_at is null
order by created_at desc
limit 1;
return jsonb_build_object(
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'skill_name', v_case.skill_name,
'skill_version', v_case.skill_version,
'candidate_range', v_case.candidate_range,
'stage', coalesce(v_case.stage, 'minute'),
'block_scan', v_case.block_scan,
'widen_declined_at_fingerprint', v_case.widen_declined_at_fingerprint,
'reported_birth_time', v_case.baseline_birth_snapshot->>'reported_birth_time',
'birth_time_source', v_case.baseline_birth_snapshot->>'birth_time_source',
'accepted_time', v_case.accepted_time,
'confirmed_time', v_case.confirmed_time,
'created_at', v_case.created_at,
'last_activity_at', v_case.last_activity_at,
'completed_at', v_case.completed_at,
'closed_reason', v_case.closed_reason,
'evidence_count', v_evidence_count,
'turn_count', v_turn_count,
'latest_result', case
when v_result.id is null then null
else jsonb_build_object(
'result_id', v_result.id,
'candidates', v_result.candidates,
'overall_confidence', v_result.overall_confidence,
'display_allowed', v_result.display_allowed,
'selection_allowed', v_result.selection_allowed,
'confirmation_allowed', v_result.confirmation_allowed,
'representative_time', v_result.representative_time,
'selected_candidate_id', v_result.selected_candidate_id,
'selected_time', v_result.selected_time,
'selection_kind', v_result.selection_kind,
'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint,
'candidate_range_fingerprint', v_result.candidate_range_fingerprint,
'skill_version', v_result.skill_version,
'algorithm_version', v_result.algorithm_version,
'event_contract_version', v_result.event_contract_version,
'decision_policy_version', v_result.decision_policy_version,
'decision_receipt', public.compose_agentic_rectification_decision_receipt(v_case.id, v_result.id, v_result.decision_receipt),
'execution_ledger', v_result.execution_ledger,
'created_at', v_result.created_at,
'invalidated_at', v_result.invalidated_at
)
end
);
end;
$$;
revoke all on function public.get_agentic_rectification_case(uuid, uuid)
from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_case(uuid, uuid)
to service_role;
create or replace function public.get_agentic_rectification_case_dossier(
p_user_id uuid,
p_case_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_turns jsonb;
v_evidence jsonb;
v_summary public.agentic_rectification_case_conversation_summaries%rowtype;
v_result public.agentic_rectification_results%rowtype;
v_evidence_count bigint;
v_turn_count bigint;
begin
if p_user_id is null or p_case_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select coalesce(jsonb_agg(jsonb_build_object(
'id', recent.id,
'role', message.role,
'text', message.text,
'status', recent.status,
'created_at', recent.created_at,
'completed_at', recent.completed_at
) order by recent.created_at, recent.id, message.ordinal), '[]'::jsonb)
into v_turns
from (
select t.*
from public.agentic_rectification_turns t
where t.case_id = v_case.id
order by t.created_at desc, t.id desc
limit 50
) recent
cross join lateral (
values
(1, 'user'::text, recent.user_message),
(2, 'assistant'::text, recent.assistant_message)
) as message(ordinal, role, text)
where message.text is not null;
select coalesce(jsonb_agg(jsonb_build_object(
'id', e.id,
'source_turn_id', e.source_turn_id,
'subject', e.subject,
'event_kind', e.event_kind,
'domain', e.domain,
'occurred_from', e.occurred_from,
'occurred_to', e.occurred_to,
'date_precision', e.date_precision,
'date_reliability', e.date_reliability,
'summary', e.summary,
'status', e.status,
'supersedes_evidence_id', e.supersedes_evidence_id,
'created_at', e.created_at
) order by e.created_at, e.id), '[]'::jsonb)
into v_evidence
from public.agentic_rectification_evidence e
where e.case_id = v_case.id;
select count(*) into v_evidence_count
from public.agentic_rectification_evidence where case_id = v_case.id;
select count(*) into v_turn_count
from public.agentic_rectification_turns where case_id = v_case.id;
select * into v_summary
from public.agentic_rectification_case_conversation_summaries
where case_id = v_case.id;
if not found then
perform public.refresh_agentic_rectification_case_conversation_summary(v_case.id);
select * into v_summary
from public.agentic_rectification_case_conversation_summaries
where case_id = v_case.id;
end if;
select * into v_result
from public.agentic_rectification_results
where case_id = v_case.id and invalidated_at is null
order by created_at desc, id desc
limit 1;
return jsonb_build_object(
'case', jsonb_build_object(
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'skill_name', v_case.skill_name,
'skill_version', v_case.skill_version,
'candidate_range', v_case.candidate_range,
'stage', coalesce(v_case.stage, 'minute'),
'block_scan', v_case.block_scan,
'widen_declined_at_fingerprint', v_case.widen_declined_at_fingerprint,
'reported_birth_time', v_case.baseline_birth_snapshot->>'reported_birth_time',
'birth_time_source', v_case.baseline_birth_snapshot->>'birth_time_source',
'accepted_time', v_case.accepted_time,
'confirmed_time', v_case.confirmed_time,
'completed_at', v_case.completed_at,
'closed_reason', v_case.closed_reason,
'last_activity_at', v_case.last_activity_at,
'evidence_count', v_evidence_count,
'turn_count', v_turn_count
),
'turns', v_turns,
'evidence', v_evidence,
'conversation_summary', jsonb_build_object(
'confirmed_evidence_summary', v_summary.confirmed_evidence_summary,
'pending_revisions', v_summary.pending_revisions,
'active_focus', v_summary.active_focus,
'declined_skipped_topics', v_summary.declined_skipped_topics,
'candidate_divergence_summary', v_summary.candidate_divergence_summary,
'missing_evidence_categories', v_summary.missing_evidence_categories,
'last_result_policy', v_summary.last_result_policy,
'summary_version', v_summary.summary_version,
'updated_at', v_summary.updated_at
),
'latest_result', case when v_result.id is null then null else jsonb_build_object(
'result_id', v_result.id,
'candidates', v_result.candidates,
'overall_confidence', v_result.overall_confidence,
'display_allowed', v_result.display_allowed,
'selection_allowed', v_result.selection_allowed,
'confirmation_allowed', v_result.confirmation_allowed,
'representative_time', v_result.representative_time,
'selected_candidate_id', v_result.selected_candidate_id,
'selected_time', v_result.selected_time,
'selection_kind', v_result.selection_kind,
'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint,
'candidate_range_fingerprint', v_result.candidate_range_fingerprint,
'skill_version', v_result.skill_version,
'algorithm_version', v_result.algorithm_version,
'event_contract_version', v_result.event_contract_version,
'decision_policy_version', v_result.decision_policy_version,
'decision_receipt', public.compose_agentic_rectification_decision_receipt(v_case.id, v_result.id, v_result.decision_receipt),
'execution_ledger', v_result.execution_ledger,
'created_at', v_result.created_at,
'invalidated_at', v_result.invalidated_at
) end
);
end;
$$;
revoke all on function public.get_agentic_rectification_case_dossier(uuid, uuid)
from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_case_dossier(uuid, uuid)
to service_role;
commit;