Files
Jyotisha/frontend/supabase/migrations/20260819010000_rectification_ingest_precision_plateau.sql
T
Jesse_Chen f708edf365
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 7m4s
fix(rectification): ingest first-turn events without dropping day precision or claiming a unique minute
SQL kinds now match the TypeScript ledger so batch ingest can confirm dated events. Confirm no longer burns the opening focus, recap uses server date labels, and the Agent sees indistinguishable width instead of a fake unique minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 07:44:23 +08:00

721 lines
24 KiB
PL/PgSQL

begin;
create or replace function public.agentic_rectification_evidence_kinds()
returns text[]
language sql
immutable
as $$
select array[
'education_start', 'education_completion', 'education_interruption',
'education_change', 'education_milestone',
'career_entry', 'career_change', 'promotion', 'career_pressure',
'career_exit', 'business_start',
'relationship_start', 'relationship_commitment', 'relationship_separation',
'relationship_end', 'relationship_change',
'relocation', 'foreign_move', 'return', 'home_change',
'finance_gain', 'finance_loss', 'income_change', 'asset_change',
'finance_change',
'self_health_event', 'pressure_period', 'family_event', 'other'
]::text[]
$$;
revoke all on function public.agentic_rectification_evidence_kinds()
from public, anon, authenticated;
grant execute on function public.agentic_rectification_evidence_kinds()
to service_role;
create or replace function public.agentic_rectification_evidence_domains()
returns text[]
language sql
immutable
as $$
select array[
'education', 'career', 'relationship', 'relocation', 'finance',
'health', 'health_pressure', 'family', 'other'
]::text[]
$$;
revoke all on function public.agentic_rectification_evidence_domains()
from public, anon, authenticated;
grant execute on function public.agentic_rectification_evidence_domains()
to service_role;
create or replace function public.agentic_rectification_date_precisions()
returns text[]
language sql
immutable
as $$
select array['year', 'month', 'quarter', 'day', 'range', 'unknown']::text[]
$$;
revoke all on function public.agentic_rectification_date_precisions()
from public, anon, authenticated;
grant execute on function public.agentic_rectification_date_precisions()
to service_role;
create or replace function public.agentic_rectification_precision_rank(p_precision text)
returns integer
language sql
immutable
as $$
select case p_precision
when 'day' then 4
when 'month' then 3
when 'quarter' then 2
when 'range' then 2
when 'year' then 1
when 'unknown' then 0
else -1
end
$$;
revoke all on function public.agentic_rectification_precision_rank(text)
from public, anon, authenticated;
grant execute on function public.agentic_rectification_precision_rank(text)
to service_role;
create or replace function public.agentic_rectification_normalize_quote(p_value text)
returns text
language sql
immutable
as $$
select regexp_replace(
lower(coalesce(p_value, '')),
'[\s\u3000,。!?、;:“”‘’()《》·—…,!.;:?]',
'',
'g'
)
$$;
do $$
declare
rec record;
begin
for rec in
select con.conname
from pg_constraint con
where con.conrelid = 'public.agentic_rectification_evidence'::regclass
and con.contype = 'c'
and pg_get_constraintdef(con.oid) like '%event_kind%'
loop
execute format(
'alter table public.agentic_rectification_evidence drop constraint %I',
rec.conname
);
end loop;
for rec in
select con.conname
from pg_constraint con
where con.conrelid = 'public.agentic_rectification_evidence'::regclass
and con.contype = 'c'
and pg_get_constraintdef(con.oid) like '%domain%'
and pg_get_constraintdef(con.oid) not like '%event_kind%'
loop
execute format(
'alter table public.agentic_rectification_evidence drop constraint %I',
rec.conname
);
end loop;
for rec in
select con.conname
from pg_constraint con
where con.conrelid = 'public.agentic_rectification_evidence'::regclass
and con.contype = 'c'
and pg_get_constraintdef(con.oid) like '%date_precision%'
loop
execute format(
'alter table public.agentic_rectification_evidence drop constraint %I',
rec.conname
);
end loop;
end;
$$;
alter table public.agentic_rectification_evidence
add constraint agentic_rectification_evidence_event_kind_check
check (event_kind = any (public.agentic_rectification_evidence_kinds()));
alter table public.agentic_rectification_evidence
add constraint agentic_rectification_evidence_domain_check
check (domain = any (public.agentic_rectification_evidence_domains()));
alter table public.agentic_rectification_evidence
add constraint agentic_rectification_evidence_date_precision_check
check (date_precision = any (public.agentic_rectification_date_precisions()));
create or replace function public.propose_agentic_rectification_evidence(
p_user_id uuid,
p_case_id uuid,
p_source_turn_id uuid,
p_user_quote text,
p_subject text,
p_event_kind text,
p_domain text,
p_occurred_from date,
p_occurred_to date,
p_date_precision text,
p_summary text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_turn public.agentic_rectification_turns%rowtype;
v_evidence_id uuid;
v_existing_id uuid;
begin
if p_user_id is null or p_case_id is null or p_source_turn_id is null
or length(btrim(coalesce(p_user_quote, ''))) = 0
or length(btrim(coalesce(p_summary, ''))) = 0
or p_subject not in ('self', 'family', 'other') then
return jsonb_build_object(
'evidence_id', null,
'idempotent', false,
'outcome', 'rejected',
'error_code', 'invalid_item',
'status', 'rejected'
);
end if;
if p_event_kind is null
or not (p_event_kind = any (public.agentic_rectification_evidence_kinds()))
or p_domain is null
or not (p_domain = any (public.agentic_rectification_evidence_domains()))
or p_date_precision is null
or not (p_date_precision = any (public.agentic_rectification_date_precisions())) then
return jsonb_build_object(
'evidence_id', null,
'idempotent', false,
'outcome', 'rejected',
'error_code', 'invalid_item',
'status', 'rejected'
);
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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
select * into v_turn
from public.agentic_rectification_turns
where id = p_source_turn_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
if v_turn.user_message is null
or position(
public.agentic_rectification_normalize_quote(p_user_quote)
in public.agentic_rectification_normalize_quote(v_turn.user_message)
) = 0 then
return jsonb_build_object(
'evidence_id', null,
'idempotent', false,
'outcome', 'rejected',
'error_code', 'quote_not_grounded',
'status', 'rejected'
);
end if;
select id into v_existing_id
from public.agentic_rectification_evidence
where case_id = p_case_id
and source_turn_id = p_source_turn_id
and user_quote = p_user_quote
and event_kind = p_event_kind
and summary = p_summary
and status in ('draft', 'pending_confirmation')
limit 1;
if v_existing_id is not null then
return jsonb_build_object(
'evidence_id', v_existing_id,
'idempotent', true,
'outcome', 'accepted',
'error_code', null,
'status', 'draft'
);
end if;
insert into public.agentic_rectification_evidence (
case_id, source_turn_id, user_quote, subject, event_kind, domain,
occurred_from, occurred_to, date_precision, summary, status
) values (
p_case_id, p_source_turn_id, p_user_quote, p_subject, p_event_kind, p_domain,
p_occurred_from, p_occurred_to, p_date_precision, p_summary, 'draft'
) returning id into v_evidence_id;
update public.agentic_rectification_cases
set last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
return jsonb_build_object(
'evidence_id', v_evidence_id,
'idempotent', false,
'outcome', 'accepted',
'error_code', null,
'status', 'draft'
);
end;
$$;
create or replace function public.confirm_agentic_rectification_evidence_v10(
p_user_id uuid,
p_case_id uuid,
p_focus_id uuid,
p_evidence_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_focus public.agentic_rectification_conversation_focuses%rowtype;
v_result jsonb;
begin
if p_user_id is null or p_case_id is null or p_evidence_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
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 not exists (
select 1 from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = p_case_id
) then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
if p_focus_id is null then
v_result := public.confirm_agentic_rectification_evidence(
p_user_id, p_case_id, p_evidence_id
);
return v_result || jsonb_build_object('focus_id', null);
end if;
select * into v_focus
from public.agentic_rectification_conversation_focuses
where id = p_focus_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
end if;
if v_focus.target_evidence_id is not null
and v_focus.target_evidence_id <> p_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
if v_focus.status not in ('active', 'resolved') then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
if v_focus.status = 'resolved'
and v_focus.target_evidence_id is distinct from p_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
v_result := public.confirm_agentic_rectification_evidence(
p_user_id, p_case_id, p_evidence_id
);
if v_focus.status = 'active' and v_focus.target_evidence_id is not null then
perform public.resolve_agentic_rectification_conversation_focus(
p_user_id, p_case_id, p_focus_id, 'resolved', p_evidence_id
);
end if;
return v_result || jsonb_build_object('focus_id', p_focus_id);
end;
$$;
create or replace function public.revise_agentic_rectification_evidence(
p_user_id uuid,
p_case_id uuid,
p_evidence_id uuid,
p_user_quote text,
p_occurred_from date,
p_occurred_to date,
p_date_precision text,
p_summary text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_target public.agentic_rectification_evidence%rowtype;
v_new_id uuid;
v_normalized text;
v_has_day boolean;
v_has_month boolean;
v_has_year boolean;
v_allows_coarser boolean;
begin
if p_user_id is null or p_case_id is null or p_evidence_id is null
or length(btrim(coalesce(p_user_quote, ''))) = 0
or length(btrim(coalesce(p_summary, ''))) = 0
or p_date_precision is null
or not (p_date_precision = any (public.agentic_rectification_date_precisions())) 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;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
select * into v_target
from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
if v_target.status not in ('confirmed', 'pending_confirmation') then
raise exception 'agentic_rectification_evidence_not_revisable' using errcode = 'P0001';
end if;
v_normalized := public.agentic_rectification_normalize_quote(p_user_quote);
if public.agentic_rectification_precision_rank(p_date_precision)
< public.agentic_rectification_precision_rank(v_target.date_precision) then
v_has_day := v_normalized ~ '[0-9]{1,2}月[0-9]{1,2}[日号]'
or coalesce(p_user_quote, '') ~ '[0-9]{4}-[0-9]{2}-[0-9]{2}';
v_has_month := v_normalized ~ '[0-9]{1,2}月'
or coalesce(p_user_quote, '') ~ '[0-9]{4}-[0-9]{2}';
v_has_year := v_normalized ~ '[0-9]{4}年'
or coalesce(p_user_quote, '') ~ '[0-9]{4}';
v_allows_coarser := case p_date_precision
when 'year' then v_has_year and not v_has_month and not v_has_day
when 'month' then v_has_month and not v_has_day
when 'quarter' then v_has_month and not v_has_day
when 'unknown' then v_normalized ~ '(不记得|记不清|不明|不知道)'
else false
end;
if not v_allows_coarser then
raise exception 'agentic_rectification_precision_downgrade' using errcode = 'P0001';
end if;
end if;
update public.agentic_rectification_evidence
set status = 'superseded',
updated_at = pg_catalog.now()
where id = v_target.id;
insert into public.agentic_rectification_evidence (
case_id, source_turn_id, user_quote, subject, event_kind, domain,
occurred_from, occurred_to, date_precision, summary, status, supersedes_evidence_id
) values (
v_target.case_id, v_target.source_turn_id, p_user_quote, v_target.subject,
v_target.event_kind, v_target.domain,
p_occurred_from, p_occurred_to, p_date_precision, p_summary,
'pending_confirmation', v_target.id
) returning id into v_new_id;
update public.agentic_rectification_cases
set last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
return jsonb_build_object(
'evidence_id', v_new_id,
'supersedes_evidence_id', v_target.id,
'idempotent', false
);
end;
$$;
create or replace function public.record_agentic_rectification_evidence_batch(
p_user_id uuid,
p_case_id uuid,
p_source_turn_id uuid,
p_focus_id uuid,
p_items jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_turn public.agentic_rectification_turns%rowtype;
v_focus public.agentic_rectification_conversation_focuses%rowtype;
v_focus_replay boolean := false;
v_focus_match_id uuid;
v_focus_match_count integer := 0;
v_focus_resolution text := 'not_requested';
v_item jsonb;
v_index bigint;
v_item_key text;
v_quote text;
v_subject text;
v_kind text;
v_domain text;
v_precision text;
v_summary text;
v_from date;
v_to date;
v_status text;
v_outcome text;
v_error text;
v_clarification jsonb;
v_existing public.agentic_rectification_evidence%rowtype;
v_evidence_id uuid;
v_idempotent boolean;
v_results jsonb := '[]'::jsonb;
v_accepted integer := 0;
v_needs integer := 0;
v_rejected integer := 0;
begin
if p_user_id is null or p_case_id is null or p_source_turn_id is null
or p_items is null or jsonb_typeof(p_items) <> 'array'
or jsonb_array_length(p_items) = 0 or jsonb_array_length(p_items) > 12 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_turn
from public.agentic_rectification_turns
where id = p_source_turn_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
if v_turn.user_message is null then
raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001';
end if;
if p_focus_id is not null then
select * into v_focus
from public.agentic_rectification_conversation_focuses
where id = p_focus_id and case_id = p_case_id
for update;
if not found or v_focus.status not in ('active', 'resolved') then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
v_focus_replay := v_focus.status = 'resolved';
if not v_focus_replay and v_focus.target_evidence_id is not null then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
v_focus_resolution := case when v_focus_replay then 'idempotent' else 'active' end;
if v_focus_replay and exists (
select 1
from jsonb_array_elements(p_items) item
where length(btrim(coalesce(item->>'idempotency_key', ''))) = 0
or not exists (
select 1 from public.agentic_rectification_evidence e
where e.case_id = p_case_id
and e.idempotency_key = btrim(item->>'idempotency_key')
)
) then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
end if;
for v_item, v_index in
select value, ordinality
from jsonb_array_elements(p_items) with ordinality
loop
v_item_key := btrim(coalesce(v_item->>'idempotency_key', ''));
v_quote := btrim(coalesce(v_item->>'quote', ''));
v_subject := coalesce(v_item->>'subject', '');
v_kind := coalesce(v_item->>'event_kind', '');
v_domain := coalesce(v_item->>'domain', '');
v_precision := coalesce(v_item->>'date_precision', '');
v_summary := btrim(coalesce(v_item->>'summary', ''));
v_from := null;
v_to := null;
v_error := null;
v_clarification := '[]'::jsonb;
v_evidence_id := null;
v_idempotent := false;
begin
if nullif(v_item->>'occurred_from', '') is not null then
v_from := (v_item->>'occurred_from')::date;
end if;
if nullif(v_item->>'occurred_to', '') is not null then
v_to := (v_item->>'occurred_to')::date;
end if;
exception when others then
v_error := 'invalid_date';
end;
if v_error is null and (
length(v_item_key) = 0 or length(v_item_key) > 160
or length(v_quote) = 0 or length(v_summary) = 0
or v_subject not in ('self', 'family', 'other')
or not (v_kind = any (public.agentic_rectification_evidence_kinds()))
or not (v_domain = any (public.agentic_rectification_evidence_domains()))
or not (v_precision = any (public.agentic_rectification_date_precisions()))
) then
v_error := 'invalid_item';
end if;
if v_error is null and position(
public.agentic_rectification_normalize_quote(v_quote)
in public.agentic_rectification_normalize_quote(v_turn.user_message)
) = 0 then
v_error := 'quote_not_grounded';
end if;
if v_error is null and v_precision = 'range'
and (v_from is null or v_to is null or v_from > v_to) then
v_error := 'invalid_range';
end if;
if v_error is not null then
v_outcome := 'rejected';
v_status := 'rejected';
v_rejected := v_rejected + 1;
else
select * into v_existing
from public.agentic_rectification_evidence
where case_id = p_case_id and idempotency_key = v_item_key;
if found then
if v_existing.source_turn_id is distinct from p_source_turn_id
or v_existing.user_quote is distinct from v_quote
or v_existing.subject is distinct from v_subject
or v_existing.event_kind is distinct from v_kind
or v_existing.domain is distinct from v_domain
or v_existing.occurred_from is distinct from v_from
or v_existing.occurred_to is distinct from v_to
or v_existing.date_precision is distinct from v_precision
or v_existing.summary is distinct from v_summary then
v_outcome := 'rejected';
v_status := 'rejected';
v_error := 'idempotency_conflict';
v_rejected := v_rejected + 1;
else
v_evidence_id := v_existing.id;
v_idempotent := true;
if v_existing.status = 'confirmed' then
v_outcome := 'accepted';
v_status := 'confirmed';
v_accepted := v_accepted + 1;
else
v_outcome := 'needs_clarification';
v_status := v_existing.status;
v_clarification := case
when v_existing.date_precision = 'unknown' then '["date"]'::jsonb
else '[]'::jsonb
end;
v_needs := v_needs + 1;
end if;
end if;
else
if v_precision = 'unknown' or v_from is null then
v_outcome := 'needs_clarification';
v_status := 'draft';
v_clarification := '["date"]'::jsonb;
v_needs := v_needs + 1;
else
v_outcome := 'accepted';
v_status := 'confirmed';
v_accepted := v_accepted + 1;
end if;
insert into public.agentic_rectification_evidence (
case_id, source_turn_id, user_quote, subject, event_kind, domain,
occurred_from, occurred_to, date_precision, summary, status,
confirmed_at, idempotency_key
) values (
p_case_id, p_source_turn_id, v_quote, v_subject, v_kind, v_domain,
v_from, v_to, v_precision, v_summary, v_status,
case when v_status = 'confirmed' then pg_catalog.now() else null end,
v_item_key
) returning id into v_evidence_id;
end if;
end if;
if p_focus_id is not null
and v_outcome = 'accepted'
and v_evidence_id is not null
and (v_focus.target_domain is null or v_domain = v_focus.target_domain)
and (v_focus.target_kind is null or v_kind = v_focus.target_kind) then
v_focus_match_count := v_focus_match_count + 1;
if v_focus_match_id is null then
v_focus_match_id := v_evidence_id;
end if;
end if;
v_results := v_results || jsonb_build_array(jsonb_build_object(
'index', v_index - 1,
'idempotency_key', nullif(v_item_key, ''),
'outcome', v_outcome,
'evidence_id', v_evidence_id,
'status', v_status,
'idempotent', v_idempotent,
'clarification_fields', v_clarification,
'error_code', v_error
));
end loop;
update public.agentic_rectification_cases
set status = case when status = 'draft' and v_accepted > 0 then 'collecting_evidence' else status end,
last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
if p_focus_id is not null then
if v_focus_replay then
if v_focus_match_count <> 1
or v_focus_match_id is distinct from v_focus.target_evidence_id then
raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
end if;
v_focus_resolution := 'idempotent';
elsif v_focus_match_count = 1 then
perform public.resolve_agentic_rectification_conversation_focus(
p_user_id, p_case_id, p_focus_id, 'resolved', v_focus_match_id
);
v_focus_resolution := 'resolved';
elsif v_focus_match_count > 1 then
v_focus_resolution := 'ambiguous';
else
v_focus_resolution := 'unmatched';
end if;
end if;
return jsonb_build_object(
'items', v_results,
'accepted_count', v_accepted,
'needs_clarification_count', v_needs,
'rejected_count', v_rejected,
'focus_id', p_focus_id,
'focus_evidence_id', v_focus_match_id,
'focus_resolution', v_focus_resolution
);
end;
$$;
commit;