begin;

create or replace function public.conversational_rectification_has_only_keys(
  p_value jsonb,
  p_allowed text[]
)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'object'
    and not exists (
      select 1
      from pg_catalog.jsonb_object_keys(p_value) key
      where not (key = any (p_allowed))
    );
$$;

create or replace function public.conversational_rectification_valid_time_text(p_value text)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select p_value ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$';
$$;

create or replace function public.conversational_rectification_valid_date_text(p_value text)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
begin
  return p_value ~ '^[1-9][0-9]{3}-[0-9]{2}-[0-9]{2}$'
    and pg_catalog.to_char(p_value::date, 'YYYY-MM-DD') = p_value;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_uuid_text(p_value text)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
begin
  if p_value !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' then
    return false;
  end if;
  perform p_value::uuid;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_text_utf16_length(
  p_value text
)
returns integer
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.char_length(p_value) + (
    select pg_catalog.count(*)::integer
    from pg_catalog.generate_series(
      1, pg_catalog.char_length(p_value)
    ) character_positions(character_index)
    where pg_catalog.octet_length(
      pg_catalog.substr(p_value, character_index, 1)
    ) = 4
  );
$$;

create or replace function public.conversational_rectification_text_is_nonblank(
  p_value text
)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.btrim(
    p_value,
    U&'\0009\000A\000B\000C\000D\0020\00A0\1680\2000\2001\2002\2003\2004\2005\2006\2007\2008\2009\200A\2028\2029\202F\205F\3000\FEFF'
  ) <> '';
$$;

create or replace function public.conversational_rectification_numbers_are_stable(
  p_value jsonb
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
declare
  v_item jsonb;
  v_number numeric;
begin
  if pg_catalog.jsonb_typeof(p_value) = 'number' then
    v_number := (p_value #>> '{}')::numeric;
    if v_number = pg_catalog.trunc(v_number) then
      return pg_catalog.abs(v_number) <= 9007199254740991;
    end if;
    return pg_catalog.abs(v_number) between 0.000001 and 1000000
      and v_number = pg_catalog.trunc(v_number, 6);
  elsif pg_catalog.jsonb_typeof(p_value) = 'array' then
    for v_item in select value from pg_catalog.jsonb_array_elements(p_value) loop
      if public.conversational_rectification_numbers_are_stable(v_item) is not true then
        return false;
      end if;
    end loop;
  elsif pg_catalog.jsonb_typeof(p_value) = 'object' then
    for v_item in select value from pg_catalog.jsonb_each(p_value) loop
      if public.conversational_rectification_numbers_are_stable(v_item) is not true then
        return false;
      end if;
    end loop;
  end if;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_text_array_is_bounded(
  p_value jsonb,
  p_max_items integer,
  p_max_characters integer,
  p_max_bytes integer
)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'array'
    and pg_catalog.jsonb_array_length(p_value) <= p_max_items
    and pg_catalog.octet_length(p_value::text) <= p_max_bytes
    and not exists (
      select 1
      from pg_catalog.jsonb_array_elements(p_value) item
      where pg_catalog.jsonb_typeof(item) is distinct from 'string'
        or public.conversational_rectification_text_utf16_length(
          item #>> '{}'
        ) not between 1 and p_max_characters
        or public.conversational_rectification_text_is_nonblank(item #>> '{}') is not true
    );
$$;

create or replace function public.conversational_rectification_valid_candidate(p_value jsonb)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
declare
  v_key text;
begin
  if pg_catalog.jsonb_typeof(p_value) is distinct from 'object'
    or pg_catalog.octet_length(p_value::text) > 512
    or public.conversational_rectification_numbers_are_stable(p_value) is not true
    or not public.conversational_rectification_has_only_keys(
      p_value,
      array['status', 'representativeTime', 'rangeStart', 'rangeEnd']::text[]
    )
    or not (p_value ?& array['status', 'representativeTime', 'rangeStart', 'rangeEnd']::text[])
    or pg_catalog.jsonb_typeof(p_value -> 'status') is distinct from 'string'
    or p_value ->> 'status' not in (
      'declared', 'pending_validation', 'ready_for_confirmation', 'confirmed'
    ) then
    return false;
  end if;
  foreach v_key in array array['representativeTime', 'rangeStart', 'rangeEnd']::text[] loop
    if p_value -> v_key <> 'null'::jsonb
      and (
        pg_catalog.jsonb_typeof(p_value -> v_key) is distinct from 'string'
        or not public.conversational_rectification_valid_time_text(p_value ->> v_key)
      ) then
      return false;
    end if;
  end loop;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_technical_receipt(
  p_value jsonb
)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'object'
    and pg_catalog.octet_length(p_value::text) <= 8192
    and public.conversational_rectification_numbers_are_stable(p_value)
    and public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'calculationVersion', 'stableLayers', 'sensitiveLayers',
        'candidateDifferenceRefs'
      ]::text[]
    )
    and p_value ?& array[
      'calculationVersion', 'stableLayers', 'sensitiveLayers',
      'candidateDifferenceRefs'
    ]::text[]
    and pg_catalog.jsonb_typeof(p_value -> 'calculationVersion') = 'string'
    and public.conversational_rectification_text_utf16_length(
      p_value ->> 'calculationVersion'
    ) between 1 and 80
    and public.conversational_rectification_text_is_nonblank(
      p_value ->> 'calculationVersion'
    )
    and public.conversational_rectification_text_array_is_bounded(
      p_value -> 'stableLayers', 20, 80, 4096
    )
    and public.conversational_rectification_text_array_is_bounded(
      p_value -> 'sensitiveLayers', 20, 80, 4096
    )
    and public.conversational_rectification_text_array_is_bounded(
      p_value -> 'candidateDifferenceRefs', 40, 120, 8192
    );
$$;

create or replace function public.conversational_rectification_valid_evidence_request(
  p_value jsonb
)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'object'
    and pg_catalog.octet_length(p_value::text) <= 2048
    and public.conversational_rectification_numbers_are_stable(p_value)
    and public.conversational_rectification_has_only_keys(
      p_value,
      array['domains', 'datePrecision', 'freeTextAllowed']::text[]
    )
    and p_value ?& array['domains', 'datePrecision', 'freeTextAllowed']::text[]
    and pg_catalog.jsonb_typeof(p_value -> 'domains') = 'array'
    and pg_catalog.jsonb_array_length(p_value -> 'domains') between 2 and 4
    and not exists (
      select 1
      from pg_catalog.jsonb_array_elements(p_value -> 'domains') domain
      where pg_catalog.jsonb_typeof(domain) is distinct from 'string'
        or domain #>> '{}' not in (
          'career', 'education', 'relocation', 'relationship', 'family', 'other'
        )
    )
    and pg_catalog.jsonb_typeof(p_value -> 'datePrecision') = 'string'
    and p_value ->> 'datePrecision' in ('month_preferred', 'year_accepted')
    and pg_catalog.jsonb_typeof(p_value -> 'freeTextAllowed') = 'boolean'
    and p_value -> 'freeTextAllowed' = 'true'::jsonb;
$$;

create or replace function public.conversational_rectification_valid_evidence_recap(
  p_value jsonb
)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'array'
    and pg_catalog.octet_length(p_value::text) <= 24576
    and public.conversational_rectification_numbers_are_stable(p_value)
    and pg_catalog.jsonb_array_length(p_value) <= 20
    and not exists (
      select 1
      from pg_catalog.jsonb_array_elements(p_value) item
      where pg_catalog.jsonb_typeof(item) <> 'object'
        or pg_catalog.octet_length(item::text) > 4096
        or not public.conversational_rectification_has_only_keys(
          item, array['id', 'summary', 'dateLabel', 'isCorrection']::text[]
        )
        or not (item ?& array['id', 'summary', 'dateLabel']::text[])
        or pg_catalog.jsonb_typeof(item -> 'id') <> 'string'
        or not public.conversational_rectification_valid_uuid_text(item ->> 'id')
        or pg_catalog.jsonb_typeof(item -> 'summary') <> 'string'
        or public.conversational_rectification_text_utf16_length(
          item ->> 'summary'
        ) not between 1 and 1000
        or public.conversational_rectification_text_is_nonblank(
          item ->> 'summary'
        ) is not true
        or pg_catalog.jsonb_typeof(item -> 'dateLabel') <> 'string'
        or public.conversational_rectification_text_utf16_length(
          item ->> 'dateLabel'
        ) not between 1 and 80
        or public.conversational_rectification_text_is_nonblank(
          item ->> 'dateLabel'
        ) is not true
        or (
          item ? 'isCorrection'
          and pg_catalog.jsonb_typeof(item -> 'isCorrection') <> 'boolean'
        )
    );
$$;

create or replace function public.conversational_rectification_valid_actions(p_value jsonb)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'array'
    and pg_catalog.octet_length(p_value::text) <= 512
    and public.conversational_rectification_numbers_are_stable(p_value)
    and pg_catalog.jsonb_array_length(p_value) <= 5
    and not exists (
      select 1
      from pg_catalog.jsonb_array_elements(p_value) action
      where pg_catalog.jsonb_typeof(action) is distinct from 'string'
        or action #>> '{}' not in (
        'answer', 'pause', 'abandon', 'confirm', 'continue_original_question'
      )
    );
$$;

create or replace function public.conversational_rectification_valid_validation_receipt(
  p_value jsonb
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
begin
  if pg_catalog.jsonb_typeof(p_value) is distinct from 'object'
    or pg_catalog.octet_length(p_value::text) > 8192
    or public.conversational_rectification_numbers_are_stable(p_value) is not true
    or not public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'modelId', 'schemaValidated', 'validatorVersion', 'validatedAt',
        'retryCount', 'fallbackUsed', 'issues'
      ]::text[]
    )
    or not (p_value ?& array['modelId', 'schemaValidated']::text[])
    or pg_catalog.jsonb_typeof(p_value -> 'modelId') is distinct from 'string'
    or public.conversational_rectification_text_utf16_length(
      p_value ->> 'modelId'
    ) not between 1 and 120
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'modelId'
    ) is not true
    or pg_catalog.jsonb_typeof(p_value -> 'schemaValidated') is distinct from 'boolean' then
    return false;
  end if;
  if p_value ? 'validatorVersion' and (
    pg_catalog.jsonb_typeof(p_value -> 'validatorVersion') is distinct from 'string'
    or public.conversational_rectification_text_utf16_length(
      p_value ->> 'validatorVersion'
    ) not between 1 and 80
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'validatorVersion'
    ) is not true
  ) then return false; end if;
  if p_value ? 'validatedAt' and (
    pg_catalog.jsonb_typeof(p_value -> 'validatedAt') is distinct from 'string'
    or public.conversational_rectification_text_utf16_length(
      p_value ->> 'validatedAt'
    ) not between 20 and 40
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'validatedAt'
    ) is not true
    or p_value ->> 'validatedAt' !~
      '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$'
    or (p_value ->> 'validatedAt')::timestamptz is null
  ) then return false; end if;
  if p_value ? 'retryCount' and (
    pg_catalog.jsonb_typeof(p_value -> 'retryCount') is distinct from 'number'
    or p_value ->> 'retryCount' !~ '^[0-9]+$'
    or (p_value ->> 'retryCount')::integer not between 0 and 2
  ) then return false; end if;
  if p_value ? 'fallbackUsed'
    and pg_catalog.jsonb_typeof(p_value -> 'fallbackUsed') is distinct from 'boolean' then
    return false;
  end if;
  if p_value ? 'issues' and not public.conversational_rectification_text_array_is_bounded(
    p_value -> 'issues', 20, 240, 8192
  ) then return false; end if;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_life_event_evidence(
  p_value jsonb
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
begin
  if pg_catalog.jsonb_typeof(p_value) is distinct from 'object'
    or pg_catalog.octet_length(p_value::text) > 16384
    or public.conversational_rectification_numbers_are_stable(p_value) is not true
    or not public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'id', 'rawText', 'domain', 'eventSummary', 'dateValue',
        'datePrecision', 'extractionStatus', 'scoreable', 'correctsEvidenceIds'
      ]::text[]
    )
    or not (p_value ?& array[
      'id', 'rawText', 'domain', 'eventSummary', 'dateValue',
      'datePrecision', 'extractionStatus'
    ]::text[])
    or pg_catalog.jsonb_typeof(p_value -> 'id') is distinct from 'string'
    or not public.conversational_rectification_valid_uuid_text(p_value ->> 'id')
    or p_value ->> 'id' !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
    or pg_catalog.jsonb_typeof(p_value -> 'rawText') is distinct from 'string'
    or public.conversational_rectification_text_utf16_length(
      p_value ->> 'rawText'
    ) not between 1 and 4000
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'rawText'
    ) is not true
    or pg_catalog.jsonb_typeof(p_value -> 'domain') is distinct from 'string'
    or p_value ->> 'domain' not in (
      'career', 'education', 'relocation', 'relationship', 'family', 'other'
    )
    or pg_catalog.jsonb_typeof(p_value -> 'eventSummary') is distinct from 'string'
    or public.conversational_rectification_text_utf16_length(
      p_value ->> 'eventSummary'
    ) not between 1 and 1000
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'eventSummary'
    ) is not true
    or not (p_value ? 'dateValue')
    or (
      p_value -> 'dateValue' <> 'null'::jsonb
      and (
        pg_catalog.jsonb_typeof(p_value -> 'dateValue') is distinct from 'string'
        or public.conversational_rectification_text_utf16_length(
          p_value ->> 'dateValue'
        ) not between 1 and 80
        or public.conversational_rectification_text_is_nonblank(
          p_value ->> 'dateValue'
        ) is not true
      )
    )
    or pg_catalog.jsonb_typeof(p_value -> 'datePrecision') is distinct from 'string'
    or p_value ->> 'datePrecision' not in ('day', 'month', 'year', 'range', 'unknown')
    or pg_catalog.jsonb_typeof(p_value -> 'extractionStatus') is distinct from 'string'
    or p_value ->> 'extractionStatus' not in (
      'clear', 'needs_clarification', 'corrected'
    )
    or (
      p_value ? 'scoreable'
      and pg_catalog.jsonb_typeof(p_value -> 'scoreable') is distinct from 'boolean'
    )
    or (
      p_value ? 'correctsEvidenceIds'
      and (
        pg_catalog.jsonb_typeof(p_value -> 'correctsEvidenceIds') is distinct from 'array'
        or pg_catalog.octet_length((p_value -> 'correctsEvidenceIds')::text) > 64
        or pg_catalog.jsonb_array_length(p_value -> 'correctsEvidenceIds') > 1
        or exists (
          select 1
          from pg_catalog.jsonb_array_elements(p_value -> 'correctsEvidenceIds') target
          where pg_catalog.jsonb_typeof(target) is distinct from 'string'
            or public.conversational_rectification_valid_uuid_text(target #>> '{}') is not true
        )
      )
    )
    or (
      p_value ->> 'extractionStatus' = 'corrected'
      and (
        not (p_value ? 'correctsEvidenceIds')
        or pg_catalog.jsonb_array_length(p_value -> 'correctsEvidenceIds') <> 1
      )
    )
    or (
      p_value ->> 'extractionStatus' = 'clear'
      and p_value ? 'correctsEvidenceIds'
      and pg_catalog.jsonb_array_length(p_value -> 'correctsEvidenceIds') <> 0
    ) then
    return false;
  end if;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_life_event_evidence_array(
  p_value jsonb
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
declare
  v_item jsonb;
begin
  if pg_catalog.jsonb_typeof(p_value) is distinct from 'array'
    or pg_catalog.jsonb_array_length(p_value) > 20 then
    return false;
  end if;
  for v_item in select value from pg_catalog.jsonb_array_elements(p_value) loop
    if public.conversational_rectification_valid_life_event_evidence(v_item) is not true then
      return false;
    end if;
  end loop;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_private_candidate(
  p_value jsonb
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
declare
  v_item jsonb;
  v_key text;
begin
  if pg_catalog.jsonb_typeof(p_value) is distinct from 'object'
    or pg_catalog.octet_length(p_value::text) > 65536
    or public.conversational_rectification_numbers_are_stable(p_value) is not true
    or not public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'resultId', 'representativeTime', 'rangeStart', 'rangeEnd',
        'calculationVersion', 'candidateWeights', 'candidateModelRefs',
        'd1Stability', 'boundaryDistanceMinutes', 'supportedSensitiveLayers',
        'scoredHistoricalEvidence', 'suggestedDomains', 'futureWindows', 'workingState'
      ]::text[]
    )
    or not (p_value ? 'calculationVersion')
    or pg_catalog.jsonb_typeof(p_value -> 'calculationVersion') is distinct from 'string'
    or public.conversational_rectification_text_utf16_length(
      p_value ->> 'calculationVersion'
    ) not between 1 and 80
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'calculationVersion'
    ) is not true then
    return false;
  end if;
  if p_value ? 'resultId' and p_value -> 'resultId' <> 'null'::jsonb and (
    pg_catalog.jsonb_typeof(p_value -> 'resultId') is distinct from 'string'
    or not public.conversational_rectification_valid_uuid_text(p_value ->> 'resultId')
  ) then return false; end if;
  foreach v_key in array array['representativeTime', 'rangeStart', 'rangeEnd']::text[] loop
    if p_value ? v_key and p_value -> v_key <> 'null'::jsonb and (
      pg_catalog.jsonb_typeof(p_value -> v_key) is distinct from 'string'
      or not public.conversational_rectification_valid_time_text(p_value ->> v_key)
    ) then return false; end if;
  end loop;
  if (p_value ? 'rangeStart') <> (p_value ? 'rangeEnd')
    or (p_value -> 'rangeStart' = 'null'::jsonb) <> (p_value -> 'rangeEnd' = 'null'::jsonb) then
    return false;
  end if;
  if p_value ? 'candidateWeights' and (
    pg_catalog.jsonb_typeof(p_value -> 'candidateWeights') is distinct from 'array'
    or pg_catalog.jsonb_array_length(p_value -> 'candidateWeights') > 1440
    or exists (
      select 1 from pg_catalog.jsonb_array_elements(p_value -> 'candidateWeights') weight
      where pg_catalog.jsonb_typeof(weight) <> 'number'
        or (weight #>> '{}')::numeric not between 0 and 1
    )
  ) then return false; end if;
  if p_value ? 'candidateModelRefs' and not public.conversational_rectification_text_array_is_bounded(
    p_value -> 'candidateModelRefs', 80, 120, 16384
  ) then return false; end if;
  if p_value ? 'supportedSensitiveLayers' and not public.conversational_rectification_text_array_is_bounded(
    p_value -> 'supportedSensitiveLayers', 40, 80, 8192
  ) then return false; end if;
  if p_value ? 'd1Stability' and p_value ->> 'd1Stability' not in (
    'stable', 'sensitive', 'unavailable'
  ) then return false; end if;
  if p_value ? 'd1Stability'
    and pg_catalog.jsonb_typeof(p_value -> 'd1Stability') is distinct from 'string' then
    return false;
  end if;
  if p_value ? 'boundaryDistanceMinutes' and p_value -> 'boundaryDistanceMinutes' <> 'null'::jsonb and (
    pg_catalog.jsonb_typeof(p_value -> 'boundaryDistanceMinutes') is distinct from 'number'
    or p_value ->> 'boundaryDistanceMinutes' !~ '^[0-9]+$'
    or (p_value ->> 'boundaryDistanceMinutes')::integer not between 0 and 1440
  ) then return false; end if;
  if p_value ? 'suggestedDomains' and (
    pg_catalog.jsonb_typeof(p_value -> 'suggestedDomains') is distinct from 'array'
    or pg_catalog.jsonb_array_length(p_value -> 'suggestedDomains') > 6
    or exists (
      select 1 from pg_catalog.jsonb_array_elements(p_value -> 'suggestedDomains') domain
      where pg_catalog.jsonb_typeof(domain) is distinct from 'string'
        or domain #>> '{}' not in (
          'career', 'education', 'relocation', 'relationship', 'family', 'other'
        )
    )
  ) then return false; end if;
  if p_value ? 'scoredHistoricalEvidence' then
    if pg_catalog.jsonb_typeof(p_value -> 'scoredHistoricalEvidence') is distinct from 'array'
      or pg_catalog.jsonb_array_length(p_value -> 'scoredHistoricalEvidence') > 100 then
      return false;
    end if;
    for v_item in select value from pg_catalog.jsonb_array_elements(p_value -> 'scoredHistoricalEvidence') loop
      if pg_catalog.jsonb_typeof(v_item) is distinct from 'object'
        or pg_catalog.octet_length(v_item::text) > 8192
        or not public.conversational_rectification_has_only_keys(
          v_item, array['evidenceId', 'domain', 'candidateTime', 'score', 'ruleRefs']::text[]
        )
        or not (v_item ?& array['evidenceId', 'domain', 'candidateTime', 'score', 'ruleRefs']::text[])
        or pg_catalog.jsonb_typeof(v_item -> 'evidenceId') is distinct from 'string'
        or not public.conversational_rectification_valid_uuid_text(v_item ->> 'evidenceId')
        or pg_catalog.jsonb_typeof(v_item -> 'domain') is distinct from 'string'
        or v_item ->> 'domain' not in ('career', 'education', 'relocation', 'relationship', 'family', 'other')
        or (v_item -> 'candidateTime' <> 'null'::jsonb and (
          pg_catalog.jsonb_typeof(v_item -> 'candidateTime') is distinct from 'string'
          or not public.conversational_rectification_valid_time_text(v_item ->> 'candidateTime')
        ))
        or pg_catalog.jsonb_typeof(v_item -> 'score') is distinct from 'number'
        or (v_item ->> 'score')::numeric not between -1000000 and 1000000
        or not public.conversational_rectification_text_array_is_bounded(
          v_item -> 'ruleRefs', 40, 120, 8192
        ) then return false; end if;
    end loop;
  end if;
  if p_value ? 'futureWindows' then
    if pg_catalog.jsonb_typeof(p_value -> 'futureWindows') is distinct from 'array'
      or pg_catalog.jsonb_array_length(p_value -> 'futureWindows') > 20 then return false; end if;
    for v_item in select value from pg_catalog.jsonb_array_elements(p_value -> 'futureWindows') loop
      if pg_catalog.jsonb_typeof(v_item) is distinct from 'object'
        or pg_catalog.octet_length(v_item::text) > 2048
        or not public.conversational_rectification_has_only_keys(
          v_item, array['label', 'startDate', 'endDate', 'scoreable']::text[]
        )
        or not (v_item ?& array['label', 'startDate', 'endDate', 'scoreable']::text[])
        or pg_catalog.jsonb_typeof(v_item -> 'label') is distinct from 'string'
        or public.conversational_rectification_text_utf16_length(
          v_item ->> 'label'
        ) not between 1 and 240
        or public.conversational_rectification_text_is_nonblank(
          v_item ->> 'label'
        ) is not true
        or pg_catalog.jsonb_typeof(v_item -> 'startDate') is distinct from 'string'
        or pg_catalog.jsonb_typeof(v_item -> 'endDate') is distinct from 'string'
        or not public.conversational_rectification_valid_date_text(v_item ->> 'startDate')
        or not public.conversational_rectification_valid_date_text(v_item ->> 'endDate')
        or v_item ->> 'startDate' > v_item ->> 'endDate'
        or v_item -> 'scoreable' <> 'false'::jsonb then return false; end if;
    end loop;
  end if;
  if p_value ? 'workingState' then
    v_item := p_value -> 'workingState';
    if pg_catalog.jsonb_typeof(v_item) is distinct from 'object'
      or pg_catalog.octet_length(v_item::text) > 8192
      or not public.conversational_rectification_has_only_keys(
        v_item, array['phase', 'iteration', 'notes']::text[]
      )
      or not (v_item ?& array['phase', 'iteration', 'notes']::text[])
      or pg_catalog.jsonb_typeof(v_item -> 'phase') is distinct from 'string'
      or v_item ->> 'phase' not in ('initial', 'collecting_evidence', 'rescoring', 'ready', 'confirmed')
      or pg_catalog.jsonb_typeof(v_item -> 'iteration') is distinct from 'number'
      or v_item ->> 'iteration' !~ '^[0-9]+$'
      or (v_item ->> 'iteration')::integer not between 0 and 100
      or not public.conversational_rectification_text_array_is_bounded(
        v_item -> 'notes', 20, 240, 8192
      ) then return false; end if;
  end if;
  return true;
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_declared_birth_input(
  p_value jsonb
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
declare
  v_place jsonb;
  v_source text;
  v_key text;
  v_before integer;
  v_after integer;
begin
  if pg_catalog.jsonb_typeof(p_value) is distinct from 'object'
    or pg_catalog.octet_length(p_value::text) > 12000
    or public.conversational_rectification_numbers_are_stable(p_value) is not true
    or not public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'birthDate', 'source', 'birthTimeClue', 'birthplace', 'reportedTime',
        'reportedPeriod', 'uncertaintyBeforeMinutes', 'uncertaintyAfterMinutes'
      ]::text[]
    )
    or not (p_value ?& array['birthDate', 'source', 'birthTimeClue', 'birthplace']::text[])
    or pg_catalog.jsonb_typeof(p_value -> 'birthDate') is distinct from 'string'
    or not public.conversational_rectification_valid_date_text(p_value ->> 'birthDate')
    or pg_catalog.jsonb_typeof(p_value -> 'source') is distinct from 'string'
    or public.conversational_rectification_text_is_nonblank(
      p_value ->> 'source'
    ) is not true
    or p_value -> 'birthTimeClue' <> 'null'::jsonb and (
      pg_catalog.jsonb_typeof(p_value -> 'birthTimeClue') is distinct from 'string'
      or public.conversational_rectification_text_utf16_length(
        p_value ->> 'birthTimeClue'
      ) not between 1 and 240
      or public.conversational_rectification_text_is_nonblank(
        p_value ->> 'birthTimeClue'
      ) is not true
    ) then return false; end if;

  v_place := p_value -> 'birthplace';
  if pg_catalog.jsonb_typeof(v_place) is distinct from 'object'
    or pg_catalog.octet_length(v_place::text) > 4096
    or not public.conversational_rectification_has_only_keys(
      v_place,
      array[
        'city', 'countryCode', 'provinceCode', 'cityCode', 'districtCode',
        'latitude', 'longitude', 'timezoneOffset'
      ]::text[]
    )
    or not (v_place ? 'timezoneOffset')
    or not ((v_place ? 'city') or (v_place ? 'cityCode'))
    or pg_catalog.jsonb_typeof(v_place -> 'timezoneOffset') is distinct from 'number'
    or (v_place ->> 'timezoneOffset')::numeric not between -12 and 14
    or (v_place ? 'latitude') <> (v_place ? 'longitude') then return false; end if;
  foreach v_key in array array['city', 'provinceCode', 'cityCode', 'districtCode']::text[] loop
    if v_place ? v_key and (
      pg_catalog.jsonb_typeof(v_place -> v_key) is distinct from 'string'
      or public.conversational_rectification_text_utf16_length(
        v_place ->> v_key
      ) not between 1 and
        case when v_key = 'city' then 120 else 80 end
      or public.conversational_rectification_text_is_nonblank(
        v_place ->> v_key
      ) is not true
    ) then return false; end if;
  end loop;
  if v_place ? 'countryCode' and (
    pg_catalog.jsonb_typeof(v_place -> 'countryCode') is distinct from 'string'
    or v_place ->> 'countryCode' !~ '^[A-Z0-9-]{1,8}$'
  ) then return false; end if;
  if v_place ? 'latitude' and (
    pg_catalog.jsonb_typeof(v_place -> 'latitude') is distinct from 'number'
    or pg_catalog.jsonb_typeof(v_place -> 'longitude') is distinct from 'number'
    or (v_place ->> 'latitude')::numeric not between -90 and 90
    or (v_place ->> 'longitude')::numeric not between -180 and 180
  ) then return false; end if;

  v_source := p_value ->> 'source';
  if v_source not in (
    'hospital_record', 'family_exact', 'approximate', 'period_only', 'unknown', 'legacy_import'
  ) then return false; end if;
  if p_value ? 'reportedTime' and (
    pg_catalog.jsonb_typeof(p_value -> 'reportedTime') is distinct from 'string'
    or not public.conversational_rectification_valid_time_text(p_value ->> 'reportedTime')
  ) then return false; end if;
  if p_value ? 'reportedPeriod' and (
    pg_catalog.jsonb_typeof(p_value -> 'reportedPeriod') is distinct from 'string'
    or p_value ->> 'reportedPeriod' not in (
      'early_morning', 'morning', 'afternoon', 'evening', 'late_night'
    )
  ) then return false; end if;
  if p_value ? 'uncertaintyBeforeMinutes' then
    if pg_catalog.jsonb_typeof(p_value -> 'uncertaintyBeforeMinutes') is distinct from 'number'
      or pg_catalog.jsonb_typeof(p_value -> 'uncertaintyAfterMinutes') is distinct from 'number'
      or p_value ->> 'uncertaintyBeforeMinutes' !~ '^[0-9]+$'
      or p_value ->> 'uncertaintyAfterMinutes' !~ '^[0-9]+$' then
      return false;
    end if;
    v_before := (p_value ->> 'uncertaintyBeforeMinutes')::integer;
    v_after := (p_value ->> 'uncertaintyAfterMinutes')::integer;
  elsif p_value ? 'uncertaintyAfterMinutes' then
    return false;
  end if;

  if v_source = 'hospital_record' then
    return p_value ? 'reportedTime'
      and not (p_value ? 'reportedPeriod')
      and v_before = 2 and v_after = 2;
  elsif v_source = 'family_exact' then
    return p_value ? 'reportedTime'
      and not (p_value ? 'reportedPeriod')
      and v_before = v_after and v_before in (5, 10, 15);
  elsif v_source = 'approximate' then
    return p_value ? 'reportedTime'
      and not (p_value ? 'reportedPeriod')
      and v_before = v_after and v_before in (15, 30, 60);
  elsif v_source = 'period_only' then
    return p_value ? 'reportedPeriod'
      and not (p_value ? 'reportedTime')
      and not (p_value ? 'uncertaintyBeforeMinutes')
      and not (p_value ? 'uncertaintyAfterMinutes');
  elsif v_source = 'unknown' then
    return not (p_value ? 'reportedPeriod')
      and not (p_value ? 'reportedTime')
      and not (p_value ? 'uncertaintyBeforeMinutes')
      and not (p_value ? 'uncertaintyAfterMinutes');
  end if;
  return not ((p_value ? 'reportedTime') and (p_value ? 'reportedPeriod'))
    and ((p_value ? 'reportedTime') or v_before is null)
    and (v_before is null or (v_before between 0 and 720 and v_after between 0 and 720));
exception when others then
  return false;
end;
$$;

create or replace function public.conversational_rectification_valid_public_turn(p_value jsonb)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'object'
    and pg_catalog.octet_length(p_value::text) <= 65536
    and public.conversational_rectification_numbers_are_stable(p_value)
    and public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'caseId', 'journeyProtocol', 'status', 'turnVersion', 'narrative',
        'candidate', 'technicalReceipt', 'evidenceRequest', 'evidenceRecap',
        'actions', 'pendingConsultationQuestion'
      ]::text[]
    )
    and p_value ?& array[
      'caseId', 'journeyProtocol', 'status', 'turnVersion', 'narrative',
      'candidate', 'technicalReceipt', 'evidenceRequest', 'evidenceRecap',
      'actions', 'pendingConsultationQuestion'
    ]::text[]
    and pg_catalog.jsonb_typeof(p_value -> 'caseId') = 'string'
    and public.conversational_rectification_valid_uuid_text(p_value ->> 'caseId')
    and pg_catalog.jsonb_typeof(p_value -> 'journeyProtocol') = 'string'
    and p_value ->> 'journeyProtocol' = 'conversational-evidence-v3'
    and pg_catalog.jsonb_typeof(p_value -> 'status') = 'string'
    and p_value ->> 'status' in ('active', 'paused', 'confirming', 'completed', 'abandoned')
    and pg_catalog.jsonb_typeof(p_value -> 'turnVersion') = 'number'
    and p_value ->> 'turnVersion' ~ '^[0-9]+$'
    and pg_catalog.jsonb_typeof(p_value -> 'narrative') = 'string'
    and public.conversational_rectification_text_utf16_length(
      p_value ->> 'narrative'
    ) between 1 and 12000
    and public.conversational_rectification_text_is_nonblank(
      p_value ->> 'narrative'
    )
    and public.conversational_rectification_valid_candidate(p_value -> 'candidate')
    and public.conversational_rectification_valid_technical_receipt(p_value -> 'technicalReceipt')
    and (
      p_value -> 'evidenceRequest' = 'null'::jsonb
      or public.conversational_rectification_valid_evidence_request(p_value -> 'evidenceRequest')
    )
    and public.conversational_rectification_valid_evidence_recap(p_value -> 'evidenceRecap')
    and public.conversational_rectification_valid_actions(p_value -> 'actions')
    and (
      p_value -> 'pendingConsultationQuestion' = 'null'::jsonb
      or (
        pg_catalog.jsonb_typeof(p_value -> 'pendingConsultationQuestion') = 'string'
        and public.conversational_rectification_text_utf16_length(
          p_value ->> 'pendingConsultationQuestion'
        ) between 1 and 500
        and public.conversational_rectification_text_is_nonblank(
          p_value ->> 'pendingConsultationQuestion'
        )
      )
    );
$$;

create or replace function public.conversational_rectification_valid_action_request(p_value jsonb)
returns boolean
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_typeof(p_value) = 'object'
    and pg_catalog.octet_length(p_value::text) <= 2048
    and public.conversational_rectification_numbers_are_stable(p_value)
    and public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'kind', 'userId', 'caseId', 'expectedVersion', 'actionId',
        'requestFingerprint', 'commandFingerprint'
      ]::text[]
    )
    and p_value ?& array[
      'kind', 'userId', 'caseId', 'expectedVersion', 'actionId', 'requestFingerprint'
    ]::text[]
    and pg_catalog.jsonb_typeof(p_value -> 'kind') = 'string'
    and p_value ->> 'kind' in (
      'create', 'save_turn', 'pause', 'abandon', 'confirm', 'import_legacy',
      'reserve_fee', 'complete_fee', 'release_fee', 'recover_fee'
    )
    and pg_catalog.jsonb_typeof(p_value -> 'userId') = 'string'
    and public.conversational_rectification_valid_uuid_text(p_value ->> 'userId')
    and pg_catalog.jsonb_typeof(p_value -> 'caseId') = 'string'
    and public.conversational_rectification_valid_uuid_text(p_value ->> 'caseId')
    and pg_catalog.jsonb_typeof(p_value -> 'actionId') = 'string'
    and public.conversational_rectification_valid_uuid_text(p_value ->> 'actionId')
    and pg_catalog.jsonb_typeof(p_value -> 'expectedVersion') = 'number'
    and p_value ->> 'expectedVersion' ~ '^[0-9]+$'
    and pg_catalog.jsonb_typeof(p_value -> 'requestFingerprint') = 'string'
    and p_value ->> 'requestFingerprint' ~ '^[0-9a-f]{64}$'
    and (
      not (p_value ? 'commandFingerprint')
      or (
        pg_catalog.jsonb_typeof(p_value -> 'commandFingerprint') = 'string'
        and p_value ->> 'commandFingerprint' ~ '^[0-9a-f]{64}$'
      )
    );
$$;

create or replace function public.conversational_rectification_action_request(
  p_kind text,
  p_user_id uuid,
  p_case_id uuid,
  p_expected_version bigint,
  p_action_id uuid,
  p_request_fingerprint text
)
returns jsonb
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.jsonb_build_object(
    'kind', p_kind,
    'userId', p_user_id,
    'caseId', p_case_id,
    'expectedVersion', p_expected_version,
    'actionId', p_action_id,
    'requestFingerprint', p_request_fingerprint
  );
$$;

create or replace function public.conversational_rectification_valid_action_response(
  p_value jsonb,
  p_action_kind text
)
returns boolean
language plpgsql
immutable
strict
set search_path = ''
as $$
begin
  if p_action_kind in ('reserve_fee', 'complete_fee', 'release_fee', 'recover_fee') then
    return coalesce(pg_catalog.jsonb_typeof(p_value) = 'object'
      and pg_catalog.octet_length(p_value::text) <= 2048
      and public.conversational_rectification_numbers_are_stable(p_value)
      and public.conversational_rectification_has_only_keys(
        p_value, array['success', 'credits', 'billing_state', 'error_code']::text[]
      )
      and p_value ?& array['success', 'credits', 'billing_state', 'error_code']::text[]
      and pg_catalog.jsonb_typeof(p_value -> 'success') = 'boolean'
      and (
        p_value -> 'credits' = 'null'::jsonb
        or (
          pg_catalog.jsonb_typeof(p_value -> 'credits') = 'number'
          and p_value ->> 'credits' ~ '^[0-9]+$'
        )
      )
      and (
        p_value -> 'billing_state' = 'null'::jsonb
        or (
          pg_catalog.jsonb_typeof(p_value -> 'billing_state') = 'string'
          and p_value ->> 'billing_state' in (
            'reserved', 'charged', 'released', 'migration_waived'
          )
        )
      )
      and (
        p_value -> 'error_code' = 'null'::jsonb
        or (
          pg_catalog.jsonb_typeof(p_value -> 'error_code') = 'string'
          and public.conversational_rectification_text_utf16_length(
            p_value ->> 'error_code'
          ) between 1 and 80
          and public.conversational_rectification_text_is_nonblank(
            p_value ->> 'error_code'
          )
        )
      ), false);
  end if;
  return coalesce(pg_catalog.jsonb_typeof(p_value) = 'object'
    and pg_catalog.octet_length(p_value::text) <= 69632
    and public.conversational_rectification_numbers_are_stable(p_value)
    and public.conversational_rectification_has_only_keys(
      p_value,
      array[
        'case_id', 'user_id', 'status', 'turn_version', 'revision_of_case_id',
        'imported_from_case_id', 'baseline_active_time', 'pending_consultation_question',
        'billing_state', 'latest_turn'
      ]::text[]
    )
    and p_value ?& array[
      'case_id', 'user_id', 'status', 'turn_version', 'revision_of_case_id',
      'imported_from_case_id', 'baseline_active_time', 'pending_consultation_question',
      'billing_state', 'latest_turn'
    ]::text[]
    and pg_catalog.jsonb_typeof(p_value -> 'case_id') = 'string'
    and public.conversational_rectification_valid_uuid_text(p_value ->> 'case_id')
    and pg_catalog.jsonb_typeof(p_value -> 'user_id') = 'string'
    and public.conversational_rectification_valid_uuid_text(p_value ->> 'user_id')
    and pg_catalog.jsonb_typeof(p_value -> 'status') = 'string'
    and p_value ->> 'status' in ('starting', 'active', 'paused', 'confirming', 'completed', 'abandoned')
    and pg_catalog.jsonb_typeof(p_value -> 'turn_version') = 'number'
    and p_value ->> 'turn_version' ~ '^[0-9]+$'
    and (p_value -> 'revision_of_case_id' = 'null'::jsonb
      or (pg_catalog.jsonb_typeof(p_value -> 'revision_of_case_id') = 'string'
        and public.conversational_rectification_valid_uuid_text(
          p_value ->> 'revision_of_case_id'
        )))
    and (p_value -> 'imported_from_case_id' = 'null'::jsonb
      or (pg_catalog.jsonb_typeof(p_value -> 'imported_from_case_id') = 'string'
        and public.conversational_rectification_valid_uuid_text(
          p_value ->> 'imported_from_case_id'
        )))
    and (p_value -> 'baseline_active_time' = 'null'::jsonb
      or (pg_catalog.jsonb_typeof(p_value -> 'baseline_active_time') = 'string'
        and public.conversational_rectification_valid_time_text(
          p_value ->> 'baseline_active_time'
        )))
    and (p_value -> 'pending_consultation_question' = 'null'::jsonb
      or (pg_catalog.jsonb_typeof(p_value -> 'pending_consultation_question') = 'string'
        and public.conversational_rectification_text_utf16_length(
          p_value ->> 'pending_consultation_question'
        ) between 1 and 500
        and public.conversational_rectification_text_is_nonblank(
          p_value ->> 'pending_consultation_question'
        )))
    and (p_value -> 'billing_state' = 'null'::jsonb
      or (pg_catalog.jsonb_typeof(p_value -> 'billing_state') = 'string'
        and p_value ->> 'billing_state' in (
          'reserved', 'charged', 'released', 'migration_waived'
        )))
    and public.conversational_rectification_valid_public_turn(p_value -> 'latest_turn'), false);
exception when others then
  return false;
end;
$$;

alter table public.birth_time_rectification_cases
  add column if not exists revision_of_case_id uuid,
  add column if not exists imported_from_case_id uuid,
  add column if not exists baseline_active_time time without time zone,
  add column if not exists pending_consultation_question text,
  add column if not exists declared_birth_input jsonb not null default '{}'::jsonb;

alter table public.birth_time_rectification_cases
  drop constraint if exists birth_time_rectification_cases_revision_of_case_id_fkey,
  drop constraint if exists birth_time_rectification_cases_imported_from_case_id_fkey,
  drop constraint if exists birth_time_rectification_cases_pending_question_check,
  drop constraint if exists birth_time_rectification_cases_declared_birth_input_check,
  drop constraint if exists birth_time_rectification_cases_private_candidate_v3_check,
  drop constraint if exists birth_time_rectification_cases_turn_state_v3_check,
  drop constraint if exists birth_time_rectification_cases_journey_snapshot_v3_check,
  drop constraint if exists birth_time_rectification_cases_journey_protocol_check,
  drop constraint if exists birth_time_rectification_cases_status_check;

alter table public.birth_time_rectification_cases
  add constraint birth_time_rectification_cases_revision_of_case_id_fkey
    foreign key (revision_of_case_id)
    references public.birth_time_rectification_cases(id) on delete set null,
  add constraint birth_time_rectification_cases_imported_from_case_id_fkey
    foreign key (imported_from_case_id)
    references public.birth_time_rectification_cases(id) on delete set null,
  add constraint birth_time_rectification_cases_pending_question_check
    check (
      pending_consultation_question is null
      or (
        public.conversational_rectification_text_utf16_length(
          pending_consultation_question
        ) between 1 and 500
        and public.conversational_rectification_text_is_nonblank(
          pending_consultation_question
        )
      )
    ),
  add constraint birth_time_rectification_cases_declared_birth_input_check
    check (
      jsonb_typeof(declared_birth_input) = 'object'
      and octet_length(declared_birth_input::text) <= 12000
      and (
        journey_protocol <> 'conversational-evidence-v3'
        or public.conversational_rectification_valid_declared_birth_input(declared_birth_input) is true
      )
    ),
  add constraint birth_time_rectification_cases_private_candidate_v3_check
    check (
      journey_protocol <> 'conversational-evidence-v3'
      or public.conversational_rectification_valid_private_candidate(candidate_result) is true
    ),
  add constraint birth_time_rectification_cases_turn_state_v3_check
    check (
      journey_protocol <> 'conversational-evidence-v3'
      or public.conversational_rectification_valid_public_turn(turn_state) is true
    ),
  add constraint birth_time_rectification_cases_journey_snapshot_v3_check
    check (
      journey_protocol <> 'conversational-evidence-v3'
      or public.conversational_rectification_valid_public_turn(journey_snapshot) is true
    ),
  add constraint birth_time_rectification_cases_journey_protocol_check
    check (journey_protocol in (
      'legacy-guided-v1',
      'dynamic-choice-v2',
      'conversational-evidence-v3'
    )),
  add constraint birth_time_rectification_cases_status_check
    check (status in (
      'assessing', 'rectifying', 'candidate', 'confirming', 'confirmed',
      'starting', 'active', 'paused', 'completed', 'abandoned'
    ));

-- Billing and case creation are phases of one public start action. Billing
-- phases use deterministic internal receipt ids so the public action can stay
-- stable without colliding with the case-transition receipt key.
create or replace function public.conversational_rectification_billing_receipt_action_id(
  p_action_id uuid,
  p_action_kind text
)
returns uuid
language sql
immutable
strict
set search_path = ''
as $$
  select pg_catalog.md5(p_action_id::text || ':' || p_action_kind)::uuid;
$$;

create table if not exists public.birth_time_rectification_turns (
  id uuid not null default gen_random_uuid(),
  case_id uuid not null
    references public.birth_time_rectification_cases(id) on delete cascade,
  turn_version bigint not null check (turn_version >= 0),
  narrative text not null check (
    public.conversational_rectification_text_utf16_length(narrative) between 1 and 12000
    and public.conversational_rectification_text_is_nonblank(narrative)
  ),
  candidate jsonb not null check (
    public.conversational_rectification_valid_candidate(candidate) is true
  ),
  technical_receipt jsonb not null check (
    public.conversational_rectification_valid_technical_receipt(technical_receipt) is true
  ),
  evidence_request jsonb check (
    evidence_request is null
    or public.conversational_rectification_valid_evidence_request(evidence_request) is true
  ),
  evidence_recap jsonb not null default '[]'::jsonb check (
    public.conversational_rectification_valid_evidence_recap(evidence_recap) is true
  ),
  actions jsonb not null default '[]'::jsonb check (
    public.conversational_rectification_valid_actions(actions) is true
  ),
  output_validation_receipt jsonb not null check (
    public.conversational_rectification_valid_validation_receipt(output_validation_receipt) is true
  ),
  created_at timestamptz not null default now(),
  primary key (case_id, turn_version),
  unique (case_id, id),
  check (not jsonb_path_exists(
    jsonb_build_object(
      'candidate', candidate,
      'technicalReceipt', technical_receipt,
      'evidenceRequest', evidence_request,
      'evidenceRecap', evidence_recap,
      'outputValidationReceipt', output_validation_receipt
    ),
    '$.**.candidateWeights'
  )),
  check (not jsonb_path_exists(candidate, '$.**.candidateScores')),
  check (not jsonb_path_exists(candidate, '$.**.partitionId')),
  check (not jsonb_path_exists(technical_receipt, '$.**.rawModelOutput')),
  check (not jsonb_path_exists(output_validation_receipt, '$.**.systemPrompt'))
);

create table if not exists public.birth_time_rectification_event_evidence (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null,
  source_turn_id uuid not null,
  raw_text text not null check (
    public.conversational_rectification_text_utf16_length(raw_text) between 1 and 4000
    and public.conversational_rectification_text_is_nonblank(raw_text)
  ),
  domain text not null check (
    domain in ('career', 'education', 'relocation', 'relationship', 'family', 'other')
  ),
  event_summary text not null check (
    public.conversational_rectification_text_utf16_length(event_summary) between 1 and 1000
    and public.conversational_rectification_text_is_nonblank(event_summary)
  ),
  date_value text check (
    date_value is null
    or (
      public.conversational_rectification_text_utf16_length(date_value) between 1 and 80
      and public.conversational_rectification_text_is_nonblank(date_value)
    )
  ),
  date_precision text not null check (
    date_precision in ('day', 'month', 'year', 'range', 'unknown')
  ),
  extraction_status text not null check (
    extraction_status in ('clear', 'needs_clarification', 'corrected')
  ),
  corrects_evidence_ids uuid[] not null default '{}'::uuid[] check (
    pg_catalog.cardinality(corrects_evidence_ids) <= 1
    and (
      (extraction_status = 'corrected' and pg_catalog.cardinality(corrects_evidence_ids) = 1)
      or (extraction_status = 'clear' and pg_catalog.cardinality(corrects_evidence_ids) = 0)
      or extraction_status = 'needs_clarification'
    )
  ),
  scoreable boolean not null default false,
  created_at timestamptz not null default now(),
  foreign key (case_id, source_turn_id)
    references public.birth_time_rectification_turns(case_id, id) on delete cascade
);

create table if not exists public.birth_time_rectification_action_receipts (
  -- Billing reservation happens before the case exists, so receipts cannot
  -- depend on the case row. The service-only RPCs preserve ownership.
  case_id uuid not null,
  action_id uuid not null,
  user_id uuid not null references auth.users(id) on delete cascade,
  action_kind text not null check (action_kind in (
    'create', 'save_turn', 'pause', 'abandon', 'confirm', 'import_legacy',
    'reserve_fee', 'complete_fee', 'release_fee', 'recover_fee'
  )),
  expected_turn_version bigint not null check (expected_turn_version >= 0),
  result_turn_version bigint not null check (result_turn_version >= 0),
  request_fingerprint text not null check (request_fingerprint ~ '^[0-9a-f]{64}$'),
  request jsonb not null check (
    public.conversational_rectification_valid_action_request(request) is true
  ),
  response jsonb not null check (
    public.conversational_rectification_valid_action_response(response, action_kind) is true
  ),
  created_at timestamptz not null default now(),
  primary key (case_id, action_id),
  check (not jsonb_path_exists(response, '$.**.candidateWeights')),
  check (not jsonb_path_exists(response, '$.**.candidateScores')),
  check (not jsonb_path_exists(response, '$.**.partitionId')),
  check (not jsonb_path_exists(response, '$.**.rawModelOutput')),
  check (not jsonb_path_exists(response, '$.**.systemPrompt'))
);

create table if not exists public.birth_time_rectification_billing (
  -- A reservation is the first durable start record and therefore precedes
  -- case creation. The case transition RPC claims this row atomically.
  case_id uuid primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  price integer not null check (price between 1 and 1000000),
  state text not null check (
    state in ('reserved', 'charged', 'released', 'migration_waived')
  ),
  reservation_id uuid unique,
  billing_receipt_id uuid unique,
  reserve_action_id uuid,
  complete_action_id uuid,
  release_action_id uuid,
  balance_after integer check (balance_after is null or balance_after >= 0),
  reserved_at timestamptz,
  charged_at timestamptz,
  released_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  check (
    state not in ('reserved', 'charged')
    or (reservation_id is not null and reserve_action_id is not null and reserved_at is not null)
  ),
  check (
    state <> 'charged'
    or (billing_receipt_id is not null and complete_action_id is not null and charged_at is not null)
  ),
  check (state <> 'released' or (release_action_id is not null and released_at is not null)),
  unique (user_id, reserve_action_id)
);

create index if not exists birth_time_rectification_cases_v3_account_idx
  on public.birth_time_rectification_cases (user_id, updated_at desc)
  where journey_protocol = 'conversational-evidence-v3';
create index if not exists birth_time_rectification_evidence_case_idx
  on public.birth_time_rectification_event_evidence (case_id, created_at);
create unique index if not exists birth_time_rectification_reserve_action_idx
  on public.birth_time_rectification_action_receipts (user_id, action_id)
  where action_kind = 'reserve_fee';

alter table public.birth_time_rectification_turns enable row level security;
alter table public.birth_time_rectification_event_evidence enable row level security;
alter table public.birth_time_rectification_action_receipts enable row level security;
alter table public.birth_time_rectification_billing enable row level security;

revoke all on table public.birth_time_rectification_cases from public, anon, authenticated;
revoke all on table public.birth_time_rectification_turns from public, anon, authenticated;
revoke all on table public.birth_time_rectification_event_evidence from public, anon, authenticated;
revoke all on table public.birth_time_rectification_action_receipts from public, anon, authenticated;
revoke all on table public.birth_time_rectification_billing from public, anon, authenticated;

revoke all on table public.birth_time_rectification_cases from service_role;
revoke all on table public.birth_time_rectification_turns from service_role;
revoke all on table public.birth_time_rectification_event_evidence from service_role;
revoke all on table public.birth_time_rectification_action_receipts from service_role;
revoke all on table public.birth_time_rectification_billing from service_role;

grant all on table public.birth_time_rectification_cases to service_role;
grant all on table public.birth_time_rectification_turns to service_role;
grant all on table public.birth_time_rectification_event_evidence to service_role;
grant all on table public.birth_time_rectification_action_receipts to service_role;
grant all on table public.birth_time_rectification_billing to service_role;

revoke all on function public.conversational_rectification_billing_receipt_action_id(
  uuid, text
) from public, anon, authenticated, service_role;
revoke all on function public.conversational_rectification_numbers_are_stable(jsonb)
  from public, anon, authenticated;
revoke all on function public.conversational_rectification_text_utf16_length(text)
  from public, anon, authenticated;
revoke all on function public.conversational_rectification_text_is_nonblank(text)
  from public, anon, authenticated;
revoke all on function public.conversational_rectification_valid_life_event_evidence(jsonb)
  from public, anon, authenticated;
revoke all on function public.conversational_rectification_valid_life_event_evidence_array(jsonb)
  from public, anon, authenticated;
grant execute on function public.conversational_rectification_numbers_are_stable(jsonb)
  to service_role;
grant execute on function public.conversational_rectification_text_utf16_length(text)
  to service_role;
grant execute on function public.conversational_rectification_text_is_nonblank(text)
  to service_role;
grant execute on function public.conversational_rectification_valid_life_event_evidence(jsonb)
  to service_role;
grant execute on function public.conversational_rectification_valid_life_event_evidence_array(jsonb)
  to service_role;

commit;
