feat: rebuild birth time rectification workflow

This commit is contained in:
Jesse_Chen
2026-07-27 09:57:14 +08:00
parent 380aba4628
commit 2ca245d643
66 changed files with 5826 additions and 699 deletions
@@ -0,0 +1,987 @@
begin;
create table public.birth_time_rectification_v4_cases (
id uuid primary key,
user_id uuid not null references auth.users(id) on delete cascade,
protocol text not null default 'rectification-evidence-v4' check (protocol = 'rectification-evidence-v4'),
version bigint not null default 0 check (version >= 0),
status text not null check (status in ('awaiting_answer', 'processing', 'range_ready', 'paused', 'abandoned')),
phase text not null check (phase in ('collecting_evidence', 'extracting_evidence', 'scoring_candidates', 'checking_robustness', 'planning_question', 'complete')),
calculation_spec jsonb not null check (jsonb_typeof(calculation_spec) = 'object' and octet_length(calculation_spec::text) <= 16384),
calculation_spec_hash text not null check (calculation_spec_hash ~ '^[a-f0-9]{64}$'),
evidence_set_hash text not null check (evidence_set_hash ~ '^[a-f0-9]{64}$'),
current_question jsonb check (current_question is null or (jsonb_typeof(current_question) = 'object' and octet_length(current_question::text) <= 4096)),
latest_snapshot_id uuid,
accepted_range_start text check (accepted_range_start is null or accepted_range_start ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'),
accepted_range_end text check (accepted_range_end is null or accepted_range_end ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
check ((accepted_range_start is null) = (accepted_range_end is null)),
check (accepted_range_start is null or accepted_range_start <> accepted_range_end)
);
create unique index birth_time_rectification_v4_one_active_case
on public.birth_time_rectification_v4_cases(user_id)
where status <> 'abandoned' and accepted_range_start is null;
create table public.birth_time_rectification_v4_actions (
user_id uuid not null references auth.users(id) on delete cascade,
action_id uuid not null,
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
job_id uuid,
created_at timestamptz not null default now(),
primary key (user_id, action_id)
);
create table public.birth_time_rectification_v4_turns (
id uuid primary key,
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
case_version bigint not null check (case_version > 0),
question_id uuid,
question_domain text check (question_domain is null or question_domain in ('education', 'relocation', 'relationship', 'career', 'finance', 'health_pressure', 'family', 'other')),
question_target_event_id uuid,
question text not null check (length(btrim(question)) between 1 and 1000),
answer text not null check (length(answer) <= 4000),
action_id uuid not null,
created_at timestamptz not null default now(),
unique (user_id, action_id)
);
create table public.birth_time_rectification_v4_events (
id uuid primary key,
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
created_at timestamptz not null default now(),
unique (case_id, id)
);
alter table public.birth_time_rectification_v4_turns
add constraint birth_time_rectification_v4_turn_target_event_fk
foreign key (case_id, question_target_event_id)
references public.birth_time_rectification_v4_events(case_id, id);
create table public.birth_time_rectification_v4_event_revisions (
id uuid primary key,
event_id uuid not null references public.birth_time_rectification_v4_events(id) on delete cascade,
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
revision integer not null check (revision > 0),
domain text not null check (domain in ('education', 'relocation', 'relationship', 'career', 'finance', 'health_pressure', 'family', 'other')),
event_kind text not null check (event_kind in ('education_milestone', 'relocation', 'relationship_start', 'relationship_end', 'career_change', 'finance_change', 'health_event', 'family_event', 'other')),
summary text not null check (length(btrim(summary)) between 1 and 1000),
raw_text text not null check (length(btrim(raw_text)) between 1 and 4000),
date_start date not null,
date_end date not null,
date_precision text not null check (date_precision in ('day', 'month', 'quarter', 'year', 'range')),
date_label text not null check (length(btrim(date_label)) between 1 and 80),
scoreability text not null check (scoreability in ('scoreable', 'context_only')),
supersedes_revision_id uuid references public.birth_time_rectification_v4_event_revisions(id),
created_at timestamptz not null default now(),
unique (event_id, revision),
check (date_start <= date_end),
check (domain not in ('family', 'other') or scoreability = 'context_only'),
check (domain <> 'relationship' or event_kind in ('relationship_start', 'relationship_end'))
);
create table public.birth_time_rectification_v4_candidate_snapshots (
id uuid primary key,
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
case_version bigint not null check (case_version >= 0),
evidence_set_hash text not null check (evidence_set_hash ~ '^[a-f0-9]{64}$'),
calculation_spec_hash text not null check (calculation_spec_hash ~ '^[a-f0-9]{64}$'),
algorithm_version text not null check (algorithm_version = 'rectification-v4-range-scoring-1'),
candidates jsonb not null check (jsonb_typeof(candidates) = 'array' and jsonb_array_length(candidates) between 1 and 1440 and octet_length(candidates::text) <= 524288),
clusters jsonb not null check (jsonb_typeof(clusters) = 'array' and jsonb_array_length(clusters) <= 20 and octet_length(clusters::text) <= 32768),
robustness jsonb not null check (jsonb_typeof(robustness) = 'object' and octet_length(robustness::text) <= 16384),
can_confirm_exact_minute boolean not null default false check (can_confirm_exact_minute = false),
can_accept_range boolean not null,
gate_reasons jsonb not null check (jsonb_typeof(gate_reasons) = 'array' and jsonb_array_length(gate_reasons) <= 20),
created_at timestamptz not null default now()
);
alter table public.birth_time_rectification_v4_cases
add constraint birth_time_rectification_v4_latest_snapshot_fk
foreign key (latest_snapshot_id) references public.birth_time_rectification_v4_candidate_snapshots(id);
create table public.birth_time_rectification_v4_jobs (
id uuid primary key,
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
turn_id uuid not null references public.birth_time_rectification_v4_turns(id) on delete cascade,
status text not null check (status in ('pending', 'processing', 'completed', 'failed', 'stale')),
phase text not null check (phase in ('collecting_evidence', 'extracting_evidence', 'scoring_candidates', 'checking_robustness', 'planning_question', 'complete')),
expected_case_version bigint not null check (expected_case_version >= 0),
evidence_set_hash text not null check (evidence_set_hash ~ '^[a-f0-9]{64}$'),
calculation_spec_hash text not null check (calculation_spec_hash ~ '^[a-f0-9]{64}$'),
worker_id uuid,
lease_expires_at timestamptz,
result_snapshot_id uuid references public.birth_time_rectification_v4_candidate_snapshots(id),
error_code text check (error_code is null or length(error_code) between 1 and 120),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.birth_time_rectification_v4_actions
add constraint birth_time_rectification_v4_actions_job_fk
foreign key (job_id) references public.birth_time_rectification_v4_jobs(id);
create index birth_time_rectification_v4_jobs_claim_idx
on public.birth_time_rectification_v4_jobs(status, created_at)
where status in ('pending', 'processing');
create table public.birth_time_rectification_v4_handoffs (
case_id uuid primary key references public.birth_time_rectification_v4_cases(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
question text not null check (length(btrim(question)) between 1 and 500),
question_fingerprint text not null check (question_fingerprint ~ '^[a-f0-9]{64}$'),
attached_case_version bigint not null check (attached_case_version >= 0),
attach_action_id uuid not null,
state text not null check (state in ('pending', 'claimed', 'executing', 'consumed')),
attempt integer not null default 0 check (attempt between 0 and 1000),
request_id uuid not null,
claim_action_id uuid,
lease_expires_at timestamptz,
claimed_at timestamptz,
consumed_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (user_id, request_id),
check ((state in ('claimed', 'executing')) = (claim_action_id is not null)),
check ((state in ('claimed', 'executing')) = (lease_expires_at is not null)),
check ((state = 'consumed') = (consumed_at is not null))
);
create table public.birth_time_rectification_v4_handoff_attach_receipts (
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
action_id uuid not null,
user_id uuid not null references auth.users(id) on delete cascade,
expected_case_version bigint not null check (expected_case_version >= 0),
question_fingerprint text not null check (question_fingerprint ~ '^[a-f0-9]{64}$'),
response jsonb not null,
created_at timestamptz not null default now(),
primary key (case_id, action_id)
);
create table public.birth_time_rectification_v4_handoff_settlements (
case_id uuid not null references public.birth_time_rectification_v4_cases(id) on delete cascade,
request_id uuid not null,
user_id uuid not null references auth.users(id) on delete cascade,
claim_action_id uuid not null,
emitted boolean not null,
response jsonb not null,
created_at timestamptz not null default now(),
primary key (case_id, request_id)
);
create index birth_time_rectification_v4_handoff_owner_state_idx
on public.birth_time_rectification_v4_handoffs(user_id, state, updated_at desc);
alter table public.birth_time_rectification_v4_cases enable row level security;
alter table public.birth_time_rectification_v4_actions enable row level security;
alter table public.birth_time_rectification_v4_turns enable row level security;
alter table public.birth_time_rectification_v4_events enable row level security;
alter table public.birth_time_rectification_v4_event_revisions enable row level security;
alter table public.birth_time_rectification_v4_candidate_snapshots enable row level security;
alter table public.birth_time_rectification_v4_jobs enable row level security;
alter table public.birth_time_rectification_v4_handoffs enable row level security;
alter table public.birth_time_rectification_v4_handoff_attach_receipts enable row level security;
alter table public.birth_time_rectification_v4_handoff_settlements enable row level security;
revoke all on table public.birth_time_rectification_v4_cases, public.birth_time_rectification_v4_actions,
public.birth_time_rectification_v4_turns, public.birth_time_rectification_v4_events,
public.birth_time_rectification_v4_event_revisions, public.birth_time_rectification_v4_candidate_snapshots,
public.birth_time_rectification_v4_jobs, public.birth_time_rectification_v4_handoffs,
public.birth_time_rectification_v4_handoff_attach_receipts,
public.birth_time_rectification_v4_handoff_settlements from public, anon, authenticated;
grant all on table public.birth_time_rectification_v4_cases, public.birth_time_rectification_v4_actions,
public.birth_time_rectification_v4_turns, public.birth_time_rectification_v4_events,
public.birth_time_rectification_v4_event_revisions, public.birth_time_rectification_v4_candidate_snapshots,
public.birth_time_rectification_v4_jobs, public.birth_time_rectification_v4_handoffs,
public.birth_time_rectification_v4_handoff_attach_receipts,
public.birth_time_rectification_v4_handoff_settlements to service_role;
create function public.create_birth_time_rectification_v4_case(
p_user_id uuid, p_case_id uuid, p_action_id uuid, p_status text, p_phase text,
p_calculation_spec jsonb, p_calculation_spec_hash text, p_evidence_set_hash text,
p_current_question jsonb, p_now timestamptz
) returns uuid
language plpgsql security definer set search_path = '' as $$
declare v_case public.birth_time_rectification_v4_cases%rowtype; v_case_id uuid;
begin
select action.case_id into v_case_id from public.birth_time_rectification_v4_actions action
where action.user_id = p_user_id and action.action_id = p_action_id;
if v_case_id is not null then return v_case_id; end if;
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(p_user_id::text || ':rectification-v4-case', 0));
select value.* into v_case from public.birth_time_rectification_v4_cases value
where value.user_id = p_user_id and value.status <> 'abandoned'
and value.accepted_range_start is null
order by value.created_at desc limit 1 for update;
if found and v_case.calculation_spec_hash = p_calculation_spec_hash then
insert into public.birth_time_rectification_v4_actions(user_id, action_id, case_id, created_at)
values (p_user_id, p_action_id, v_case.id, p_now);
return v_case.id;
end if;
if found then
update public.birth_time_rectification_v4_cases set
status = 'abandoned', phase = 'complete', current_question = null, updated_at = p_now
where id = v_case.id;
update public.birth_time_rectification_v4_jobs set
status = 'stale', lease_expires_at = null, updated_at = p_now
where case_id = v_case.id and status in ('pending', 'processing');
end if;
insert into public.birth_time_rectification_v4_cases (
id, user_id, status, phase, calculation_spec, calculation_spec_hash,
evidence_set_hash, current_question, created_at, updated_at
) values (
p_case_id, p_user_id, p_status, p_phase, p_calculation_spec, p_calculation_spec_hash,
p_evidence_set_hash, p_current_question, p_now, p_now
);
insert into public.birth_time_rectification_v4_actions(user_id, action_id, case_id, created_at)
values (p_user_id, p_action_id, p_case_id, p_now);
return p_case_id;
end;
$$;
create function public.submit_birth_time_rectification_v4_answer(
p_user_id uuid, p_case_id uuid, p_action_id uuid, p_expected_version bigint,
p_turn_id uuid, p_question_id uuid, p_question_domain text, p_question_target_event_id uuid, p_question text,
p_answer text, p_job_id uuid, p_now timestamptz
) returns uuid
language plpgsql security definer set search_path = '' as $$
declare v_case public.birth_time_rectification_v4_cases%rowtype; v_job_id uuid;
begin
select action.job_id into v_job_id from public.birth_time_rectification_v4_actions action
where action.user_id = p_user_id and action.action_id = p_action_id;
if v_job_id is not null then return v_job_id; end if;
select value.* into v_case from public.birth_time_rectification_v4_cases value
where value.id = p_case_id and value.user_id = p_user_id for update;
if not found then raise exception 'rectification_v4_case_not_found'; end if;
if v_case.version <> p_expected_version then raise exception 'stale_rectification_v4_case'; end if;
if v_case.status not in ('awaiting_answer', 'range_ready') then raise exception 'rectification_v4_case_not_awaiting_answer'; end if;
insert into public.birth_time_rectification_v4_turns(
id, case_id, user_id, case_version, question_id, question_domain, question_target_event_id, question, answer, action_id, created_at
) values (
p_turn_id, p_case_id, p_user_id, p_expected_version + 1, p_question_id, p_question_domain, p_question_target_event_id,
p_question, p_answer, p_action_id, p_now
);
update public.birth_time_rectification_v4_cases set
version = p_expected_version + 1, status = 'processing', phase = 'extracting_evidence',
current_question = null, updated_at = p_now
where id = p_case_id;
insert into public.birth_time_rectification_v4_jobs(
id, case_id, user_id, turn_id, status, phase, expected_case_version,
evidence_set_hash, calculation_spec_hash, created_at, updated_at
) values (
p_job_id, p_case_id, p_user_id, p_turn_id, 'pending', 'extracting_evidence', p_expected_version + 1,
v_case.evidence_set_hash, v_case.calculation_spec_hash, p_now, p_now
);
insert into public.birth_time_rectification_v4_actions(user_id, action_id, case_id, job_id, created_at)
values (p_user_id, p_action_id, p_case_id, p_job_id, p_now);
return p_job_id;
end;
$$;
create function public.revise_birth_time_rectification_v4_event(
p_user_id uuid, p_case_id uuid, p_action_id uuid, p_expected_version bigint,
p_revision jsonb, p_output_evidence_set_hash text, p_turn_id uuid, p_job_id uuid, p_now timestamptz
) returns uuid
language plpgsql security definer set search_path = '' as $$
declare v_case public.birth_time_rectification_v4_cases%rowtype; v_job_id uuid; v_event_id uuid;
begin
select action.job_id into v_job_id from public.birth_time_rectification_v4_actions action
where action.user_id = p_user_id and action.action_id = p_action_id;
if v_job_id is not null then return v_job_id; end if;
select value.* into v_case from public.birth_time_rectification_v4_cases value
where value.id = p_case_id and value.user_id = p_user_id for update;
if not found then raise exception 'rectification_v4_case_not_found'; end if;
if v_case.version <> p_expected_version then raise exception 'stale_rectification_v4_case'; end if;
if v_case.status in ('processing', 'abandoned', 'paused') then raise exception 'rectification_v4_case_invalid_state'; end if;
if jsonb_typeof(p_revision) <> 'object' then raise exception 'invalid_rectification_v4_event_revision'; end if;
v_event_id = (p_revision->>'eventId')::uuid;
insert into public.birth_time_rectification_v4_events(id, case_id, user_id, created_at)
values (v_event_id, p_case_id, p_user_id, p_now) on conflict (id) do nothing;
insert into public.birth_time_rectification_v4_event_revisions(
id, event_id, case_id, user_id, revision, domain, event_kind, summary, raw_text,
date_start, date_end, date_precision, date_label, scoreability, supersedes_revision_id, created_at
) values (
(p_revision->>'id')::uuid, v_event_id, p_case_id, p_user_id,
(p_revision->>'revision')::integer, p_revision->>'domain', p_revision->>'eventKind',
p_revision->>'summary', p_revision->>'rawText',
(p_revision#>>'{dateRange,start}')::date, (p_revision#>>'{dateRange,end}')::date,
p_revision#>>'{dateRange,precision}', p_revision#>>'{dateRange,label}', p_revision->>'scoreability',
nullif(p_revision->>'supersedesRevisionId', '')::uuid, (p_revision->>'createdAt')::timestamptz
);
insert into public.birth_time_rectification_v4_turns(
id, case_id, user_id, case_version, question, answer, action_id, created_at
) values (p_turn_id, p_case_id, p_user_id, p_expected_version + 1, '修订事件', '', p_action_id, p_now);
update public.birth_time_rectification_v4_cases set
version = p_expected_version + 1, status = 'processing', phase = 'scoring_candidates',
evidence_set_hash = p_output_evidence_set_hash, current_question = null, updated_at = p_now
where id = p_case_id;
insert into public.birth_time_rectification_v4_jobs(
id, case_id, user_id, turn_id, status, phase, expected_case_version,
evidence_set_hash, calculation_spec_hash, created_at, updated_at
) values (
p_job_id, p_case_id, p_user_id, p_turn_id, 'pending', 'scoring_candidates', p_expected_version + 1,
p_output_evidence_set_hash, v_case.calculation_spec_hash, p_now, p_now
);
insert into public.birth_time_rectification_v4_actions(user_id, action_id, case_id, job_id, created_at)
values (p_user_id, p_action_id, p_case_id, p_job_id, p_now);
return p_job_id;
end;
$$;
create function public.transition_birth_time_rectification_v4_case(
p_user_id uuid, p_case_id uuid, p_action_id uuid, p_expected_version bigint,
p_status text, p_phase text, p_accepted_range_start text, p_accepted_range_end text, p_now timestamptz
) returns uuid
language plpgsql security definer set search_path = '' as $$
declare v_case public.birth_time_rectification_v4_cases%rowtype; v_case_id uuid; v_snapshot public.birth_time_rectification_v4_candidate_snapshots%rowtype; v_primary jsonb;
begin
select action.case_id into v_case_id from public.birth_time_rectification_v4_actions action
where action.user_id = p_user_id and action.action_id = p_action_id;
if v_case_id is not null then return v_case_id; end if;
select value.* into v_case from public.birth_time_rectification_v4_cases value
where value.id = p_case_id and value.user_id = p_user_id for update;
if not found then raise exception 'rectification_v4_case_not_found'; end if;
if v_case.version <> p_expected_version then raise exception 'stale_rectification_v4_case'; end if;
if v_case.status = 'abandoned' then raise exception 'rectification_v4_case_invalid_state'; end if;
if p_status = 'paused' and v_case.status not in ('awaiting_answer', 'range_ready') then raise exception 'rectification_v4_case_invalid_state'; end if;
if p_status = 'awaiting_answer' and v_case.status <> 'paused' then raise exception 'rectification_v4_case_invalid_state'; end if;
if p_status = 'range_ready' then
if v_case.status not in ('awaiting_answer', 'range_ready') or v_case.latest_snapshot_id is null then raise exception 'rectification_v4_case_invalid_state'; end if;
select value.* into v_snapshot from public.birth_time_rectification_v4_candidate_snapshots value where value.id = v_case.latest_snapshot_id;
v_primary = v_snapshot.clusters->0;
if not v_snapshot.can_accept_range
or p_accepted_range_start is null or p_accepted_range_end is null
or p_accepted_range_start = p_accepted_range_end
or v_primary->>'startTime' <> p_accepted_range_start
or v_primary->>'endTime' <> p_accepted_range_end then
raise exception 'rectification_v4_range_not_acceptable';
end if;
elsif p_accepted_range_start is not null or p_accepted_range_end is not null then
raise exception 'rectification_v4_range_not_acceptable';
end if;
update public.birth_time_rectification_v4_cases set
version = p_expected_version + 1, status = p_status, phase = p_phase,
accepted_range_start = case when p_status = 'range_ready' then p_accepted_range_start else accepted_range_start end,
accepted_range_end = case when p_status = 'range_ready' then p_accepted_range_end else accepted_range_end end,
current_question = case when p_status in ('abandoned', 'range_ready') then null else current_question end,
updated_at = p_now
where id = p_case_id;
insert into public.birth_time_rectification_v4_actions(user_id, action_id, case_id, created_at)
values (p_user_id, p_action_id, p_case_id, p_now);
return p_case_id;
end;
$$;
create function public.claim_next_birth_time_rectification_v4_job(
p_worker_id uuid, p_now timestamptz
) returns uuid
language plpgsql security definer set search_path = '' as $$
declare v_job_id uuid;
begin
select value.id into v_job_id from public.birth_time_rectification_v4_jobs value
where value.status = 'pending'
or (value.status = 'processing' and value.lease_expires_at <= p_now)
order by value.created_at for update skip locked limit 1;
if v_job_id is null then return null; end if;
update public.birth_time_rectification_v4_jobs set
status = 'processing', worker_id = p_worker_id, lease_expires_at = p_now + interval '10 minutes',
error_code = null, updated_at = p_now
where id = v_job_id;
return v_job_id;
end;
$$;
create function public.update_birth_time_rectification_v4_job_phase(
p_worker_id uuid, p_job_id uuid, p_phase text, p_now timestamptz
) returns void
language plpgsql security definer set search_path = '' as $$
declare v_case_id uuid;
begin
update public.birth_time_rectification_v4_jobs set phase = p_phase, updated_at = p_now,
lease_expires_at = p_now + interval '10 minutes'
where id = p_job_id and worker_id = p_worker_id and status = 'processing'
and lease_expires_at > p_now returning case_id into v_case_id;
if v_case_id is null then raise exception 'rectification_v4_job_lease_lost'; end if;
update public.birth_time_rectification_v4_cases set phase = p_phase, updated_at = p_now where id = v_case_id;
end;
$$;
create function public.complete_birth_time_rectification_v4_job(
p_worker_id uuid, p_job_id uuid, p_expected_case_version bigint,
p_input_evidence_set_hash text, p_output_evidence_set_hash text, p_calculation_spec_hash text,
p_event_revisions jsonb, p_snapshot jsonb, p_next_question jsonb,
p_status text, p_phase text, p_now timestamptz
) returns uuid
language plpgsql security definer set search_path = '' as $$
declare
v_job public.birth_time_rectification_v4_jobs%rowtype;
v_case public.birth_time_rectification_v4_cases%rowtype;
item jsonb;
v_snapshot_id uuid;
begin
select value.* into v_job from public.birth_time_rectification_v4_jobs value
where value.id = p_job_id for update;
if not found or v_job.worker_id is distinct from p_worker_id or v_job.status <> 'processing'
or v_job.lease_expires_at <= p_now then raise exception 'rectification_v4_job_lease_lost'; end if;
select value.* into v_case from public.birth_time_rectification_v4_cases value
where value.id = v_job.case_id for update;
if v_case.version <> p_expected_case_version
or v_case.evidence_set_hash <> p_input_evidence_set_hash
or v_case.calculation_spec_hash <> p_calculation_spec_hash
or v_job.expected_case_version <> p_expected_case_version
or v_job.evidence_set_hash <> p_input_evidence_set_hash
or v_job.calculation_spec_hash <> p_calculation_spec_hash then
update public.birth_time_rectification_v4_jobs set status = 'stale', updated_at = p_now where id = p_job_id;
raise exception 'stale_rectification_v4_job';
end if;
if jsonb_typeof(p_event_revisions) <> 'array' then raise exception 'invalid_rectification_v4_event_revisions'; end if;
for item in select value from jsonb_array_elements(p_event_revisions) loop
insert into public.birth_time_rectification_v4_events(id, case_id, user_id, created_at)
values ((item->>'eventId')::uuid, v_case.id, v_case.user_id, (item->>'createdAt')::timestamptz)
on conflict (id) do nothing;
insert into public.birth_time_rectification_v4_event_revisions(
id, event_id, case_id, user_id, revision, domain, event_kind, summary, raw_text,
date_start, date_end, date_precision, date_label, scoreability, supersedes_revision_id, created_at
) values (
(item->>'id')::uuid, (item->>'eventId')::uuid, v_case.id, v_case.user_id,
(item->>'revision')::integer, item->>'domain', item->>'eventKind', item->>'summary', item->>'rawText',
(item#>>'{dateRange,start}')::date, (item#>>'{dateRange,end}')::date,
item#>>'{dateRange,precision}', item#>>'{dateRange,label}', item->>'scoreability',
nullif(item->>'supersedesRevisionId', '')::uuid, (item->>'createdAt')::timestamptz
);
end loop;
if p_snapshot is not null then
v_snapshot_id = (p_snapshot->>'id')::uuid;
if (p_snapshot->>'canConfirmExactMinute')::boolean then raise exception 'exact_minute_confirmation_forbidden'; end if;
insert into public.birth_time_rectification_v4_candidate_snapshots(
id, case_id, user_id, case_version, evidence_set_hash, calculation_spec_hash,
algorithm_version, candidates, clusters, robustness, can_confirm_exact_minute,
can_accept_range, gate_reasons, created_at
) values (
v_snapshot_id, v_case.id, v_case.user_id, (p_snapshot->>'caseVersion')::bigint,
p_snapshot->>'evidenceSetHash', p_snapshot->>'calculationSpecHash', p_snapshot->>'algorithmVersion',
p_snapshot->'candidates', p_snapshot->'clusters', p_snapshot->'robustness', false,
(p_snapshot->>'canAcceptRange')::boolean, p_snapshot->'gateReasons', (p_snapshot->>'createdAt')::timestamptz
);
end if;
update public.birth_time_rectification_v4_cases set
version = p_expected_case_version + 1, evidence_set_hash = p_output_evidence_set_hash,
latest_snapshot_id = coalesce(v_snapshot_id, latest_snapshot_id), current_question = p_next_question,
status = p_status, phase = p_phase, updated_at = p_now
where id = v_case.id;
update public.birth_time_rectification_v4_jobs set
status = 'completed', phase = p_phase, result_snapshot_id = v_snapshot_id,
lease_expires_at = null, updated_at = p_now
where id = p_job_id;
return v_case.id;
end;
$$;
create function public.fail_birth_time_rectification_v4_job(
p_worker_id uuid, p_job_id uuid, p_expected_case_version bigint, p_error_code text,
p_restore_question jsonb, p_now timestamptz
) returns void
language plpgsql security definer set search_path = '' as $$
declare v_case_id uuid;
begin
update public.birth_time_rectification_v4_jobs set
status = 'failed', error_code = p_error_code, lease_expires_at = null, updated_at = p_now
where id = p_job_id and worker_id = p_worker_id and status = 'processing'
returning case_id into v_case_id;
if v_case_id is null then raise exception 'rectification_v4_job_lease_lost'; end if;
update public.birth_time_rectification_v4_cases set
status = 'awaiting_answer', phase = 'collecting_evidence', current_question = p_restore_question, updated_at = p_now
where id = v_case_id and version = p_expected_case_version;
end;
$$;
create function public.birth_time_rectification_v4_handoff_projection(
p_user_id uuid,
p_case_id uuid
) returns jsonb
language sql stable security definer set search_path = '' as $$
select pg_catalog.jsonb_build_object(
'protocol', 'rectification-evidence-v4',
'caseId', handoff.case_id,
'caseVersion', case_value.version,
'question', handoff.question,
'questionFingerprint', handoff.question_fingerprint,
'requestId', handoff.request_id,
'status', case
when handoff.state = 'pending' then 'pending'
when handoff.state in ('claimed', 'executing')
and handoff.lease_expires_at <= pg_catalog.now() then 'pending'
when handoff.state in ('claimed', 'executing') then 'in_progress'
else 'consumed'
end,
'acceptedRange', case
when case_value.accepted_range_start is null then null
else pg_catalog.jsonb_build_object(
'start', case_value.accepted_range_start,
'end', case_value.accepted_range_end
)
end
)
from public.birth_time_rectification_v4_handoffs handoff
join public.birth_time_rectification_v4_cases case_value
on case_value.id = handoff.case_id and case_value.user_id = handoff.user_id
where handoff.case_id = p_case_id and handoff.user_id = p_user_id;
$$;
create function public.attach_birth_time_rectification_v4_question(
p_user_id uuid,
p_case_id uuid,
p_expected_version bigint,
p_action_id uuid,
p_question text,
p_question_fingerprint text
) returns jsonb
language plpgsql security definer set search_path = '' as $$
declare
v_case public.birth_time_rectification_v4_cases%rowtype;
v_handoff public.birth_time_rectification_v4_handoffs%rowtype;
v_receipt public.birth_time_rectification_v4_handoff_attach_receipts%rowtype;
v_response jsonb;
begin
if p_user_id is null or p_case_id is null or p_action_id is null
or p_expected_version is null or p_expected_version < 0
or p_question is null or length(btrim(p_question)) not between 1 and 500
or p_question_fingerprint is null or p_question_fingerprint !~ '^[a-f0-9]{64}$'
or public.conversational_rectification_question_fingerprint(p_question)
is distinct from p_question_fingerprint then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(
p_user_id::text || ':' || p_case_id::text || ':rectification-v4-attach', 0
)
);
select case_value.* into v_case
from public.birth_time_rectification_v4_cases case_value
where case_value.id = p_case_id and case_value.user_id = p_user_id
for update;
if not found then
raise exception 'rectification_v4_case_not_found' using errcode = 'P0001';
end if;
select receipt.* into v_receipt
from public.birth_time_rectification_v4_handoff_attach_receipts receipt
where receipt.case_id = p_case_id and receipt.action_id = p_action_id
for update;
if found then
if v_receipt.user_id is distinct from p_user_id
or v_receipt.expected_case_version is distinct from p_expected_version
or v_receipt.question_fingerprint is distinct from p_question_fingerprint then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
return v_receipt.response;
end if;
if v_case.version is distinct from p_expected_version then
raise exception 'stale_rectification_v4_case' using errcode = 'P0001';
end if;
if v_case.status = 'abandoned' then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
select handoff.* into v_handoff
from public.birth_time_rectification_v4_handoffs handoff
where handoff.case_id = p_case_id and handoff.user_id = p_user_id
for update;
if found then
if v_handoff.question_fingerprint is distinct from p_question_fingerprint
or v_handoff.question is distinct from p_question then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
else
insert into public.birth_time_rectification_v4_handoffs (
case_id, user_id, question, question_fingerprint,
attached_case_version, attach_action_id, state, attempt, request_id
) values (
p_case_id, p_user_id, p_question, p_question_fingerprint,
p_expected_version, p_action_id, 'pending', 0,
public.conversational_rectification_handoff_request_id(p_case_id, 0)
);
end if;
v_response := public.birth_time_rectification_v4_handoff_projection(
p_user_id, p_case_id
);
insert into public.birth_time_rectification_v4_handoff_attach_receipts (
case_id, action_id, user_id, expected_case_version,
question_fingerprint, response
) values (
p_case_id, p_action_id, p_user_id, p_expected_version,
p_question_fingerprint, v_response
);
return v_response;
end;
$$;
create function public.load_birth_time_rectification_v4_handoff(
p_user_id uuid,
p_case_id uuid default null
) returns jsonb
language plpgsql stable security definer set search_path = '' as $$
declare v_case_id uuid;
begin
if p_user_id is null then return null; end if;
if p_case_id is not null then
v_case_id := p_case_id;
else
select handoff.case_id into v_case_id
from public.birth_time_rectification_v4_handoffs handoff
where handoff.user_id = p_user_id and handoff.state <> 'consumed'
order by handoff.updated_at desc, handoff.created_at desc
limit 1;
end if;
return public.birth_time_rectification_v4_handoff_projection(p_user_id, v_case_id);
end;
$$;
create function public.consume_birth_time_rectification_v4_handoff(
p_user_id uuid,
p_case_id uuid
) returns void
language plpgsql security definer set search_path = '' as $$
begin
update public.birth_time_rectification_v4_handoffs
set state = 'consumed', claim_action_id = null, lease_expires_at = null,
consumed_at = coalesce(consumed_at, pg_catalog.now()), updated_at = pg_catalog.now()
where case_id = p_case_id and user_id = p_user_id;
end;
$$;
create function public.claim_birth_time_rectification_v4_handoff(
p_user_id uuid,
p_case_id uuid,
p_expected_version bigint,
p_action_id uuid,
p_question_fingerprint text
) returns jsonb
language plpgsql security definer set search_path = '' as $$
declare
v_case public.birth_time_rectification_v4_cases%rowtype;
v_handoff public.birth_time_rectification_v4_handoffs%rowtype;
v_request_status text;
begin
if p_user_id is null or p_case_id is null or p_action_id is null
or p_expected_version is null or p_expected_version < 0
or p_question_fingerprint is null or p_question_fingerprint !~ '^[a-f0-9]{64}$' then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(
p_user_id::text || ':' || p_case_id::text || ':rectification-v4-claim', 0
)
);
select case_value.* into v_case
from public.birth_time_rectification_v4_cases case_value
where case_value.id = p_case_id and case_value.user_id = p_user_id
for update;
if not found then
raise exception 'rectification_v4_case_not_found' using errcode = 'P0001';
end if;
if v_case.version is distinct from p_expected_version then
raise exception 'stale_rectification_v4_case' using errcode = 'P0001';
end if;
if v_case.status is distinct from 'range_ready'
or v_case.phase is distinct from 'complete'
or v_case.accepted_range_start is null
or v_case.accepted_range_end is null then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
select handoff.* into v_handoff
from public.birth_time_rectification_v4_handoffs handoff
where handoff.case_id = p_case_id and handoff.user_id = p_user_id
for update;
if not found or v_handoff.question_fingerprint is distinct from p_question_fingerprint then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
if v_handoff.state = 'consumed' then
return public.birth_time_rectification_v4_handoff_projection(p_user_id, p_case_id);
end if;
if v_handoff.state in ('claimed', 'executing')
and v_handoff.lease_expires_at > pg_catalog.now() then
if v_handoff.state = 'claimed' and v_handoff.claim_action_id = p_action_id then
return public.birth_time_rectification_v4_handoff_projection(p_user_id, p_case_id)
|| pg_catalog.jsonb_build_object('status', 'claimed');
end if;
return public.birth_time_rectification_v4_handoff_projection(p_user_id, p_case_id)
|| pg_catalog.jsonb_build_object('status', 'in_progress');
end if;
select request.status into v_request_status
from public.consultation_requests request
where request.user_id = p_user_id and request.request_id = v_handoff.request_id::text
for update;
if v_request_status = 'completed' then
perform public.consume_birth_time_rectification_v4_handoff(p_user_id, p_case_id);
return public.birth_time_rectification_v4_handoff_projection(p_user_id, p_case_id);
end if;
if v_request_status = 'cancelled' then
update public.birth_time_rectification_v4_handoffs
set attempt = attempt + 1,
request_id = public.conversational_rectification_handoff_request_id(
p_case_id, attempt + 1
)
where case_id = p_case_id and user_id = p_user_id
returning * into v_handoff;
end if;
update public.birth_time_rectification_v4_handoffs
set state = 'claimed', claim_action_id = p_action_id,
lease_expires_at = pg_catalog.now() + interval '2 minutes',
claimed_at = pg_catalog.now(), consumed_at = null, updated_at = pg_catalog.now()
where case_id = p_case_id and user_id = p_user_id;
return public.birth_time_rectification_v4_handoff_projection(p_user_id, p_case_id)
|| pg_catalog.jsonb_build_object('status', 'claimed');
end;
$$;
create function public.begin_birth_time_rectification_v4_handoff_execution(
p_user_id uuid,
p_case_id uuid,
p_expected_version bigint,
p_claim_action_id uuid,
p_request_id uuid,
p_question_fingerprint text
) returns jsonb
language plpgsql security definer set search_path = '' as $$
declare
v_case public.birth_time_rectification_v4_cases%rowtype;
v_handoff public.birth_time_rectification_v4_handoffs%rowtype;
v_request_status text;
v_credits integer;
v_accepted_range jsonb;
begin
if p_user_id is null or p_case_id is null or p_claim_action_id is null
or p_request_id is null or p_expected_version is null or p_expected_version < 0
or p_question_fingerprint is null or p_question_fingerprint !~ '^[a-f0-9]{64}$' then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(
p_user_id::text || ':' || p_case_id::text || ':rectification-v4-execute', 0
)
);
select case_value.* into v_case
from public.birth_time_rectification_v4_cases case_value
where case_value.id = p_case_id and case_value.user_id = p_user_id
for update;
select handoff.* into v_handoff
from public.birth_time_rectification_v4_handoffs handoff
where handoff.case_id = p_case_id and handoff.user_id = p_user_id
for update;
if v_case.id is null or v_handoff.case_id is null
or v_case.version is distinct from p_expected_version
or v_case.status is distinct from 'range_ready'
or v_case.phase is distinct from 'complete'
or v_case.accepted_range_start is null
or v_case.accepted_range_end is null
or v_handoff.question_fingerprint is distinct from p_question_fingerprint
or v_handoff.request_id is distinct from p_request_id then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
v_accepted_range := pg_catalog.jsonb_build_object(
'start', v_case.accepted_range_start,
'end', v_case.accepted_range_end
);
if v_handoff.state = 'consumed' then
return pg_catalog.jsonb_build_object(
'status', 'consumed', 'requestId', p_request_id, 'acceptedRange', v_accepted_range
);
end if;
if v_handoff.claim_action_id is distinct from p_claim_action_id
or v_handoff.lease_expires_at <= pg_catalog.now() then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
if v_handoff.state = 'executing' then
return pg_catalog.jsonb_build_object(
'status', 'in_progress', 'requestId', p_request_id, 'acceptedRange', v_accepted_range
);
end if;
if v_handoff.state is distinct from 'claimed' then
return pg_catalog.jsonb_build_object(
'status', 'in_progress', 'requestId', p_request_id, 'acceptedRange', v_accepted_range
);
end if;
select request.status into v_request_status
from public.consultation_requests request
where request.user_id = p_user_id and request.request_id = p_request_id::text
for update;
if v_request_status = 'completed' then
perform public.consume_birth_time_rectification_v4_handoff(p_user_id, p_case_id);
return pg_catalog.jsonb_build_object(
'status', 'consumed', 'requestId', p_request_id, 'acceptedRange', v_accepted_range
);
end if;
if v_request_status = 'cancelled' then
update public.birth_time_rectification_v4_handoffs
set state = 'pending', attempt = attempt + 1,
request_id = public.conversational_rectification_handoff_request_id(
p_case_id, attempt + 1
),
claim_action_id = null, lease_expires_at = null,
claimed_at = null, updated_at = pg_catalog.now()
where case_id = p_case_id and user_id = p_user_id;
return pg_catalog.jsonb_build_object(
'status', 'released', 'requestId', p_request_id, 'acceptedRange', v_accepted_range
);
end if;
select profile.credits into v_credits
from public.profiles profile where profile.id = p_user_id;
update public.birth_time_rectification_v4_handoffs
set state = 'executing', lease_expires_at = pg_catalog.now() + interval '2 minutes',
updated_at = pg_catalog.now()
where case_id = p_case_id and user_id = p_user_id;
return pg_catalog.jsonb_build_object(
'status', 'ready',
'requestId', p_request_id,
'billingReused', coalesce(v_request_status = 'reserved', false),
'credits', v_credits,
'acceptedRange', v_accepted_range
);
end;
$$;
create function public.settle_birth_time_rectification_v4_handoff(
p_user_id uuid,
p_case_id uuid,
p_claim_action_id uuid,
p_request_id uuid,
p_emitted boolean
) returns jsonb
language plpgsql security definer set search_path = '' as $$
declare
v_handoff public.birth_time_rectification_v4_handoffs%rowtype;
v_receipt public.birth_time_rectification_v4_handoff_settlements%rowtype;
v_success boolean;
v_credits integer;
v_error text;
v_response jsonb;
begin
if p_user_id is null or p_case_id is null or p_claim_action_id is null
or p_request_id is null or p_emitted is null then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(
p_user_id::text || ':' || p_case_id::text || ':rectification-v4-settle', 0
)
);
select settlement.* into v_receipt
from public.birth_time_rectification_v4_handoff_settlements settlement
where settlement.case_id = p_case_id and settlement.request_id = p_request_id
for update;
if found then
if v_receipt.user_id is distinct from p_user_id
or v_receipt.claim_action_id is distinct from p_claim_action_id
or v_receipt.emitted is distinct from p_emitted then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
return v_receipt.response;
end if;
select handoff.* into v_handoff
from public.birth_time_rectification_v4_handoffs handoff
where handoff.case_id = p_case_id and handoff.user_id = p_user_id
for update;
if not found or v_handoff.request_id is distinct from p_request_id
or v_handoff.claim_action_id is distinct from p_claim_action_id
or v_handoff.state not in ('claimed', 'executing') then
raise exception 'rectification_v4_handoff_conflict' using errcode = 'P0001';
end if;
if p_emitted then
select result.success, result.credits, result.error_code
into v_success, v_credits, v_error
from public.complete_consultation_credit(p_user_id, p_request_id::text) result;
if v_success is not true then
raise exception 'rectification_v4_billing_failed' using errcode = 'P0001';
end if;
perform public.consume_birth_time_rectification_v4_handoff(p_user_id, p_case_id);
v_response := pg_catalog.jsonb_build_object(
'status', 'consumed', 'requestId', p_request_id, 'credits', v_credits
);
else
select result.success, result.credits, result.error_code
into v_success, v_credits, v_error
from public.cancel_consultation_credit(p_user_id, p_request_id::text) result;
if v_success is not true then
raise exception 'rectification_v4_billing_failed' using errcode = 'P0001';
end if;
update public.birth_time_rectification_v4_handoffs
set state = 'pending', attempt = attempt + 1,
request_id = public.conversational_rectification_handoff_request_id(
p_case_id, attempt + 1
),
claim_action_id = null, lease_expires_at = null,
claimed_at = null, updated_at = pg_catalog.now()
where case_id = p_case_id and user_id = p_user_id;
v_response := pg_catalog.jsonb_build_object(
'status', 'pending', 'requestId', p_request_id, 'credits', v_credits
);
end if;
insert into public.birth_time_rectification_v4_handoff_settlements (
case_id, request_id, user_id, claim_action_id, emitted, response
) values (
p_case_id, p_request_id, p_user_id, p_claim_action_id, p_emitted, v_response
);
return v_response;
end;
$$;
revoke all on function public.create_birth_time_rectification_v4_case(uuid, uuid, uuid, text, text, jsonb, text, text, jsonb, timestamptz) from public, anon, authenticated;
revoke all on function public.submit_birth_time_rectification_v4_answer(uuid, uuid, uuid, bigint, uuid, uuid, text, uuid, text, text, uuid, timestamptz) from public, anon, authenticated;
revoke all on function public.revise_birth_time_rectification_v4_event(uuid, uuid, uuid, bigint, jsonb, text, uuid, uuid, timestamptz) from public, anon, authenticated;
revoke all on function public.transition_birth_time_rectification_v4_case(uuid, uuid, uuid, bigint, text, text, text, text, timestamptz) from public, anon, authenticated;
revoke all on function public.claim_next_birth_time_rectification_v4_job(uuid, timestamptz) from public, anon, authenticated;
revoke all on function public.update_birth_time_rectification_v4_job_phase(uuid, uuid, text, timestamptz) from public, anon, authenticated;
revoke all on function public.complete_birth_time_rectification_v4_job(uuid, uuid, bigint, text, text, text, jsonb, jsonb, jsonb, text, text, timestamptz) from public, anon, authenticated;
revoke all on function public.fail_birth_time_rectification_v4_job(uuid, uuid, bigint, text, jsonb, timestamptz) from public, anon, authenticated;
revoke all on function public.birth_time_rectification_v4_handoff_projection(uuid, uuid) from public, anon, authenticated, service_role;
revoke all on function public.consume_birth_time_rectification_v4_handoff(uuid, uuid) from public, anon, authenticated, service_role;
revoke all on function public.attach_birth_time_rectification_v4_question(uuid, uuid, bigint, uuid, text, text) from public, anon, authenticated;
revoke all on function public.load_birth_time_rectification_v4_handoff(uuid, uuid) from public, anon, authenticated;
revoke all on function public.claim_birth_time_rectification_v4_handoff(uuid, uuid, bigint, uuid, text) from public, anon, authenticated;
revoke all on function public.begin_birth_time_rectification_v4_handoff_execution(uuid, uuid, bigint, uuid, uuid, text) from public, anon, authenticated;
revoke all on function public.settle_birth_time_rectification_v4_handoff(uuid, uuid, uuid, uuid, boolean) from public, anon, authenticated;
grant execute on function public.create_birth_time_rectification_v4_case(uuid, uuid, uuid, text, text, jsonb, text, text, jsonb, timestamptz) to service_role;
grant execute on function public.submit_birth_time_rectification_v4_answer(uuid, uuid, uuid, bigint, uuid, uuid, text, uuid, text, text, uuid, timestamptz) to service_role;
grant execute on function public.revise_birth_time_rectification_v4_event(uuid, uuid, uuid, bigint, jsonb, text, uuid, uuid, timestamptz) to service_role;
grant execute on function public.transition_birth_time_rectification_v4_case(uuid, uuid, uuid, bigint, text, text, text, text, timestamptz) to service_role;
grant execute on function public.claim_next_birth_time_rectification_v4_job(uuid, timestamptz) to service_role;
grant execute on function public.update_birth_time_rectification_v4_job_phase(uuid, uuid, text, timestamptz) to service_role;
grant execute on function public.complete_birth_time_rectification_v4_job(uuid, uuid, bigint, text, text, text, jsonb, jsonb, jsonb, text, text, timestamptz) to service_role;
grant execute on function public.fail_birth_time_rectification_v4_job(uuid, uuid, bigint, text, jsonb, timestamptz) to service_role;
grant execute on function public.attach_birth_time_rectification_v4_question(uuid, uuid, bigint, uuid, text, text) to service_role;
grant execute on function public.load_birth_time_rectification_v4_handoff(uuid, uuid) to service_role;
grant execute on function public.claim_birth_time_rectification_v4_handoff(uuid, uuid, bigint, uuid, text) to service_role;
grant execute on function public.begin_birth_time_rectification_v4_handoff_execution(uuid, uuid, bigint, uuid, uuid, text) to service_role;
grant execute on function public.settle_birth_time_rectification_v4_handoff(uuid, uuid, uuid, uuid, boolean) to service_role;
commit;