fix: align conversational persistence validators
This commit is contained in:
@@ -1,16 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const POSTGRES_JSON_DECIMAL_SCALE = 1_000_000;
|
||||
const POSTGRES_JSON_MIN_FRACTION = 1 / POSTGRES_JSON_DECIMAL_SCALE;
|
||||
const POSTGRES_JSON_MAX_DECIMAL_PLACES = 6;
|
||||
const POSTGRES_JSON_MIN_FRACTION = 0.000001;
|
||||
const POSTGRES_JSON_MAX_FRACTION_MAGNITUDE = 1_000_000;
|
||||
|
||||
function canonicalJsonDecimalPlaces(value: number): number | null {
|
||||
const serialized = JSON.stringify(value);
|
||||
const match = /^-?\d+(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(serialized);
|
||||
if (!match) return null;
|
||||
const fractionLength = match[1]?.length ?? 0;
|
||||
const exponent = Number(match[2] ?? 0);
|
||||
return Math.max(0, fractionLength - exponent);
|
||||
}
|
||||
|
||||
function postgresStableJsonNumber(value: number): boolean {
|
||||
if (!Number.isFinite(value)) return false;
|
||||
if (Number.isSafeInteger(value)) return true;
|
||||
const magnitude = Math.abs(value);
|
||||
const decimalPlaces = canonicalJsonDecimalPlaces(value);
|
||||
return magnitude >= POSTGRES_JSON_MIN_FRACTION
|
||||
&& magnitude <= POSTGRES_JSON_MAX_FRACTION_MAGNITUDE
|
||||
&& Number.isSafeInteger(value * POSTGRES_JSON_DECIMAL_SCALE);
|
||||
&& decimalPlaces !== null
|
||||
&& decimalPlaces <= POSTGRES_JSON_MAX_DECIMAL_PLACES;
|
||||
}
|
||||
|
||||
function postgresJsonNumbersAreStable(
|
||||
|
||||
@@ -37,7 +37,7 @@ const birthplaceSchema = boundedJson(z.object({
|
||||
}
|
||||
}), 4_096);
|
||||
|
||||
const clueSchema = z.string().max(240).nullable();
|
||||
const clueSchema = boundedText(240).nullable();
|
||||
const commonBirthFields = {
|
||||
birthDate: birthDateSchema,
|
||||
birthTimeClue: clueSchema,
|
||||
|
||||
+166
-49
@@ -51,6 +51,9 @@ 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
|
||||
@@ -58,6 +61,26 @@ exception when others then
|
||||
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
|
||||
)
|
||||
@@ -67,10 +90,10 @@ immutable
|
||||
strict
|
||||
set search_path = ''
|
||||
as $$
|
||||
select pg_catalog.char_length(pg_catalog.btrim(
|
||||
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'
|
||||
)) > 0;
|
||||
) <> '';
|
||||
$$;
|
||||
|
||||
create or replace function public.conversational_rectification_numbers_are_stable(
|
||||
@@ -130,9 +153,11 @@ as $$
|
||||
and not exists (
|
||||
select 1
|
||||
from pg_catalog.jsonb_array_elements(p_value) item
|
||||
where pg_catalog.jsonb_typeof(item) <> 'string'
|
||||
or pg_catalog.char_length(item #>> '{}') not between 1 and p_max_characters
|
||||
or pg_catalog.char_length(pg_catalog.btrim(item #>> '{}')) = 0
|
||||
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
|
||||
);
|
||||
$$;
|
||||
|
||||
@@ -154,6 +179,7 @@ begin
|
||||
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
|
||||
@@ -198,8 +224,12 @@ as $$
|
||||
'candidateDifferenceRefs'
|
||||
]::text[]
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'calculationVersion') = 'string'
|
||||
and pg_catalog.char_length(p_value ->> 'calculationVersion') between 1 and 80
|
||||
and pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'calculationVersion')) > 0
|
||||
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
|
||||
)
|
||||
@@ -232,10 +262,15 @@ as $$
|
||||
and pg_catalog.jsonb_array_length(p_value -> 'domains') between 2 and 4
|
||||
and not exists (
|
||||
select 1
|
||||
from pg_catalog.jsonb_array_elements_text(p_value -> 'domains') domain
|
||||
where domain not in ('career', 'education', 'relocation', 'relationship', 'family', 'other')
|
||||
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;
|
||||
$$;
|
||||
|
||||
@@ -264,11 +299,19 @@ as $$
|
||||
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 pg_catalog.char_length(item ->> 'summary') not between 1 and 1000
|
||||
or pg_catalog.char_length(pg_catalog.btrim(item ->> 'summary')) = 0
|
||||
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 pg_catalog.char_length(item ->> 'dateLabel') not between 1 and 80
|
||||
or pg_catalog.char_length(pg_catalog.btrim(item ->> 'dateLabel')) = 0
|
||||
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
|
||||
);
|
||||
$$;
|
||||
|
||||
@@ -285,8 +328,9 @@ as $$
|
||||
and pg_catalog.jsonb_array_length(p_value) <= 5
|
||||
and not exists (
|
||||
select 1
|
||||
from pg_catalog.jsonb_array_elements_text(p_value) action
|
||||
where action not in (
|
||||
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'
|
||||
)
|
||||
);
|
||||
@@ -314,19 +358,32 @@ begin
|
||||
)
|
||||
or not (p_value ?& array['modelId', 'schemaValidated']::text[])
|
||||
or pg_catalog.jsonb_typeof(p_value -> 'modelId') is distinct from 'string'
|
||||
or pg_catalog.char_length(p_value ->> 'modelId') not between 1 and 120
|
||||
or pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'modelId')) = 0
|
||||
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 pg_catalog.char_length(p_value ->> 'validatorVersion') not between 1 and 80
|
||||
or pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'validatorVersion')) = 0
|
||||
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 pg_catalog.char_length(p_value ->> 'validatedAt') not between 20 and 40
|
||||
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
|
||||
@@ -377,7 +434,9 @@ begin
|
||||
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 pg_catalog.char_length(p_value ->> 'rawText') not between 1 and 4000
|
||||
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
|
||||
@@ -386,7 +445,9 @@ begin
|
||||
'career', 'education', 'relocation', 'relationship', 'family', 'other'
|
||||
)
|
||||
or pg_catalog.jsonb_typeof(p_value -> 'eventSummary') is distinct from 'string'
|
||||
or pg_catalog.char_length(p_value ->> 'eventSummary') not between 1 and 1000
|
||||
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
|
||||
@@ -395,7 +456,9 @@ begin
|
||||
p_value -> 'dateValue' <> 'null'::jsonb
|
||||
and (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'dateValue') is distinct from 'string'
|
||||
or pg_catalog.char_length(p_value ->> 'dateValue') not between 1 and 80
|
||||
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
|
||||
@@ -473,8 +536,12 @@ begin
|
||||
)
|
||||
or not (p_value ? 'calculationVersion')
|
||||
or pg_catalog.jsonb_typeof(p_value -> 'calculationVersion') is distinct from 'string'
|
||||
or pg_catalog.char_length(p_value ->> 'calculationVersion') not between 1 and 80
|
||||
or pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'calculationVersion')) = 0 then
|
||||
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 (
|
||||
@@ -522,8 +589,11 @@ begin
|
||||
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_text(p_value -> 'suggestedDomains') domain
|
||||
where domain not in ('career', 'education', 'relocation', 'relationship', 'family', 'other')
|
||||
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
|
||||
@@ -564,8 +634,12 @@ begin
|
||||
)
|
||||
or not (v_item ?& array['label', 'startDate', 'endDate', 'scoreable']::text[])
|
||||
or pg_catalog.jsonb_typeof(v_item -> 'label') is distinct from 'string'
|
||||
or pg_catalog.char_length(v_item ->> 'label') not between 1 and 240
|
||||
or pg_catalog.char_length(pg_catalog.btrim(v_item ->> 'label')) = 0
|
||||
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')
|
||||
@@ -627,9 +701,17 @@ begin
|
||||
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 pg_catalog.char_length(p_value ->> 'birthTimeClue') > 240
|
||||
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';
|
||||
@@ -650,9 +732,13 @@ begin
|
||||
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 pg_catalog.char_length(v_place ->> v_key) not between 1 and
|
||||
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 pg_catalog.char_length(pg_catalog.btrim(v_place ->> v_key)) = 0
|
||||
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 (
|
||||
@@ -747,14 +833,21 @@ as $$
|
||||
'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 pg_catalog.char_length(p_value ->> 'narrative') between 1 and 12000
|
||||
and pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'narrative')) > 0
|
||||
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 (
|
||||
@@ -767,10 +860,12 @@ as $$
|
||||
p_value -> 'pendingConsultationQuestion' = 'null'::jsonb
|
||||
or (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'pendingConsultationQuestion') = 'string'
|
||||
and pg_catalog.char_length(p_value ->> 'pendingConsultationQuestion') between 1 and 500
|
||||
and pg_catalog.char_length(pg_catalog.btrim(
|
||||
and public.conversational_rectification_text_utf16_length(
|
||||
p_value ->> 'pendingConsultationQuestion'
|
||||
)) > 0
|
||||
) between 1 and 500
|
||||
and public.conversational_rectification_text_is_nonblank(
|
||||
p_value ->> 'pendingConsultationQuestion'
|
||||
)
|
||||
)
|
||||
);
|
||||
$$;
|
||||
@@ -864,14 +959,23 @@ begin
|
||||
)
|
||||
and (
|
||||
p_value -> 'billing_state' = 'null'::jsonb
|
||||
or p_value ->> 'billing_state' in ('reserved', 'charged', 'released', 'migration_waived')
|
||||
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 pg_catalog.char_length(p_value ->> 'error_code') between 1 and 80
|
||||
and pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'error_code')) > 0
|
||||
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;
|
||||
@@ -916,10 +1020,12 @@ begin
|
||||
)))
|
||||
and (p_value -> 'pending_consultation_question' = 'null'::jsonb
|
||||
or (pg_catalog.jsonb_typeof(p_value -> 'pending_consultation_question') = 'string'
|
||||
and pg_catalog.char_length(p_value ->> 'pending_consultation_question') between 1 and 500
|
||||
and pg_catalog.char_length(pg_catalog.btrim(
|
||||
and public.conversational_rectification_text_utf16_length(
|
||||
p_value ->> 'pending_consultation_question'
|
||||
)) > 0))
|
||||
) 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 (
|
||||
@@ -959,7 +1065,14 @@ alter table public.birth_time_rectification_cases
|
||||
add constraint birth_time_rectification_cases_pending_question_check
|
||||
check (
|
||||
pending_consultation_question is null
|
||||
or char_length(pending_consultation_question) between 1 and 500
|
||||
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 (
|
||||
@@ -1019,8 +1132,8 @@ create table if not exists public.birth_time_rectification_turns (
|
||||
references public.birth_time_rectification_cases(id) on delete cascade,
|
||||
turn_version bigint not null check (turn_version >= 0),
|
||||
narrative text not null check (
|
||||
char_length(narrative) between 1 and 12000
|
||||
and char_length(btrim(narrative)) > 0
|
||||
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
|
||||
@@ -1065,20 +1178,20 @@ create table if not exists public.birth_time_rectification_event_evidence (
|
||||
case_id uuid not null,
|
||||
source_turn_id uuid not null,
|
||||
raw_text text not null check (
|
||||
char_length(raw_text) between 1 and 4000
|
||||
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 (
|
||||
char_length(event_summary) between 1 and 1000
|
||||
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 (
|
||||
char_length(date_value) between 1 and 80
|
||||
public.conversational_rectification_text_utf16_length(date_value) between 1 and 80
|
||||
and public.conversational_rectification_text_is_nonblank(date_value)
|
||||
)
|
||||
),
|
||||
@@ -1191,6 +1304,8 @@ revoke all on function public.conversational_rectification_billing_receipt_actio
|
||||
) 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)
|
||||
@@ -1199,6 +1314,8 @@ revoke all on function public.conversational_rectification_valid_life_event_evid
|
||||
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)
|
||||
|
||||
@@ -478,10 +478,11 @@ test("durable JSON uses PostgreSQL-stable numeric vectors recursively", () => {
|
||||
const stableVector = {
|
||||
zero: 0,
|
||||
minFraction: 0.000001,
|
||||
ieeeRoundingBoundary: 1.000001,
|
||||
decimal: 0.123456,
|
||||
maxSafe: 9_007_199_254_740_991,
|
||||
};
|
||||
assert.equal(postgresJsonbTextBytes(stableVector), 86);
|
||||
assert.equal(postgresJsonbTextBytes(stableVector), 120);
|
||||
assert.equal(declaredBirthInputSchema.safeParse({
|
||||
...storedRow.declared_birth_input,
|
||||
birthplace: {
|
||||
@@ -492,7 +493,8 @@ test("durable JSON uses PostgreSQL-stable numeric vectors recursively", () => {
|
||||
},
|
||||
}).success, true);
|
||||
|
||||
for (const candidateWeights of [[1e-100], [0.1234567]]) {
|
||||
assert.ok(Number.isSafeInteger(1.000001 * 1_000_000) === false);
|
||||
for (const candidateWeights of [[1e-7], [1e-100], [0.0000012], [0.1234567]]) {
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId,
|
||||
calculationVersion: "rectification-v3.1",
|
||||
@@ -520,6 +522,87 @@ test("durable JSON uses PostgreSQL-stable numeric vectors recursively", () => {
|
||||
assert.equal(postgresJsonbTextBytes({ nested: [{ score: 1e-100 }] }), Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
test("durable text uses ECMAScript nonblank and UTF-16 maximum semantics", () => {
|
||||
const unicodeWhitespace = "\u00a0\u2007\ufeff";
|
||||
const validAstral80 = "😀".repeat(40);
|
||||
const invalidAstral82 = "😀".repeat(41);
|
||||
|
||||
assert.equal(validationReceiptSchema.safeParse({
|
||||
modelId: "😀".repeat(60),
|
||||
schemaValidated: true,
|
||||
}).success, true);
|
||||
assert.equal(validationReceiptSchema.safeParse({
|
||||
modelId: "😀".repeat(61),
|
||||
schemaValidated: true,
|
||||
}).success, false);
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId,
|
||||
calculationVersion: validAstral80,
|
||||
}).success, true);
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId,
|
||||
calculationVersion: invalidAstral82,
|
||||
}).success, false);
|
||||
|
||||
for (const invalid of [
|
||||
{ ...firstTurn, narrative: unicodeWhitespace },
|
||||
{
|
||||
...firstTurn,
|
||||
technicalReceipt: { ...firstTurn.technicalReceipt, calculationVersion: unicodeWhitespace },
|
||||
},
|
||||
{
|
||||
...firstTurn,
|
||||
technicalReceipt: { ...firstTurn.technicalReceipt, stableLayers: [unicodeWhitespace] },
|
||||
},
|
||||
{
|
||||
...firstTurn,
|
||||
evidenceRecap: [{ id: resultId, summary: unicodeWhitespace, dateLabel: "2020-01" }],
|
||||
},
|
||||
{ ...firstTurn, pendingConsultationQuestion: unicodeWhitespace },
|
||||
]) {
|
||||
assert.equal(conversationalRectificationTurnSchema.safeParse(invalid).success, false);
|
||||
}
|
||||
|
||||
for (const invalid of [
|
||||
{ modelId: unicodeWhitespace, schemaValidated: true },
|
||||
{ modelId: "model", schemaValidated: true, validatorVersion: unicodeWhitespace },
|
||||
{ modelId: "model", schemaValidated: true, issues: [unicodeWhitespace] },
|
||||
]) {
|
||||
assert.equal(validationReceiptSchema.safeParse(invalid).success, false);
|
||||
}
|
||||
|
||||
for (const invalid of [
|
||||
{ resultId, calculationVersion: unicodeWhitespace },
|
||||
{ resultId, calculationVersion: "v1", candidateModelRefs: [unicodeWhitespace] },
|
||||
{
|
||||
resultId,
|
||||
calculationVersion: "v1",
|
||||
futureWindows: [{
|
||||
label: unicodeWhitespace,
|
||||
startDate: "2020-01-01",
|
||||
endDate: "2020-01-02",
|
||||
scoreable: false,
|
||||
}],
|
||||
},
|
||||
{
|
||||
resultId,
|
||||
calculationVersion: "v1",
|
||||
workingState: { phase: "initial", iteration: 0, notes: [unicodeWhitespace] },
|
||||
},
|
||||
]) {
|
||||
assert.equal(privateCandidateSchema.safeParse(invalid).success, false);
|
||||
}
|
||||
|
||||
assert.equal(declaredBirthInputSchema.safeParse({
|
||||
...storedRow.declared_birth_input,
|
||||
birthTimeClue: unicodeWhitespace,
|
||||
}).success, false);
|
||||
assert.equal(declaredBirthInputSchema.safeParse({
|
||||
...storedRow.declared_birth_input,
|
||||
birthplace: { ...storedRow.declared_birth_input.birthplace, city: unicodeWhitespace },
|
||||
}).success, false);
|
||||
});
|
||||
|
||||
test("evidence recap enforces the SQL-matched aggregate byte limit", () => {
|
||||
const evidenceRecap = Array.from({ length: 9 }, (_, index) => ({
|
||||
id: `00000000-0000-4000-8000-${(700 + index).toString().padStart(12, "0")}`,
|
||||
@@ -630,6 +713,29 @@ test("durable private and receipt schemas accept boundaries and reject oversize
|
||||
}).success, false);
|
||||
});
|
||||
|
||||
test("durable UUID text is canonical hyphenated syntax and case-insensitive", () => {
|
||||
const canonical = "a9890e09-d535-46f0-9a36-86017515a5a1";
|
||||
const compact = canonical.replaceAll("-", "");
|
||||
const braced = `{${canonical}}`;
|
||||
|
||||
for (const validResultId of [canonical, canonical.toUpperCase()]) {
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId: validResultId,
|
||||
calculationVersion: "v1",
|
||||
}).success, true);
|
||||
}
|
||||
for (const invalidResultId of [compact, braced]) {
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId: invalidResultId,
|
||||
calculationVersion: "v1",
|
||||
}).success, false);
|
||||
assert.equal(conversationalRectificationTurnSchema.safeParse({
|
||||
...firstTurn,
|
||||
evidenceRecap: [{ id: invalidResultId, summary: "summary", dateLabel: "2020-01" }],
|
||||
}).success, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("public turn JSON fields reject field and byte boundary violations", () => {
|
||||
assert.equal(conversationalRectificationActionReceiptResponseSchema.safeParse({
|
||||
...storedRow,
|
||||
|
||||
@@ -47,8 +47,8 @@ def test_v3_schema_is_account_scoped_bounded_and_service_role_only() -> None:
|
||||
)
|
||||
|
||||
assert "primary key (case_id, turn_version)" in sql
|
||||
assert "char_length(narrative) between 1 and 12000" in sql
|
||||
assert "char_length(raw_text) between 1 and 4000" in sql
|
||||
assert "conversational_rectification_text_utf16_length(narrative) between 1 and 12000" in sql
|
||||
assert "conversational_rectification_text_utf16_length(raw_text) between 1 and 4000" in sql
|
||||
assert "date_precision in ('day', 'month', 'year', 'range', 'unknown')" in sql
|
||||
assert "extraction_status in ('clear', 'needs_clarification', 'corrected')" in sql
|
||||
assert "primary key (case_id, action_id)" in sql
|
||||
@@ -310,6 +310,13 @@ def test_durable_json_numbers_and_evidence_recap_share_postgres_bounds() -> None
|
||||
sql = _normalized(SCHEMA)
|
||||
|
||||
assert "create or replace function public.conversational_rectification_numbers_are_stable" in sql
|
||||
numeric = _function(sql, "conversational_rectification_numbers_are_stable")
|
||||
for invariant in (
|
||||
"abs(v_number) <= 9007199254740991",
|
||||
"abs(v_number) between 0.000001 and 1000000",
|
||||
"v_number = pg_catalog.trunc(v_number, 6)",
|
||||
):
|
||||
assert invariant in numeric
|
||||
for validator in (
|
||||
"conversational_rectification_valid_declared_birth_input",
|
||||
"conversational_rectification_valid_private_candidate",
|
||||
@@ -322,6 +329,71 @@ def test_durable_json_numbers_and_evidence_recap_share_postgres_bounds() -> None
|
||||
assert "octet_length(p_value::text) <= 24576" in recap
|
||||
|
||||
|
||||
def test_sql_json_strings_and_enum_arrays_reject_null_and_non_string_values() -> None:
|
||||
sql = _normalized(SCHEMA)
|
||||
assert "jsonb_array_elements_text" not in sql
|
||||
|
||||
candidate = _function(sql, "conversational_rectification_valid_candidate")
|
||||
assert "jsonb_typeof(p_value -> 'status') is distinct from 'string'" in candidate
|
||||
|
||||
request = _function(sql, "conversational_rectification_valid_evidence_request")
|
||||
assert "jsonb_typeof(p_value -> 'dateprecision') = 'string'" in request
|
||||
assert "jsonb_array_elements(p_value -> 'domains')" in request
|
||||
assert "jsonb_typeof(domain) is distinct from 'string'" in request
|
||||
|
||||
actions = _function(sql, "conversational_rectification_valid_actions")
|
||||
assert "jsonb_array_elements(p_value)" in actions
|
||||
assert "jsonb_typeof(action) is distinct from 'string'" in actions
|
||||
|
||||
private = _function(sql, "conversational_rectification_valid_private_candidate")
|
||||
assert "jsonb_array_elements(p_value -> 'suggesteddomains')" in private
|
||||
assert "jsonb_typeof(domain) is distinct from 'string'" in private
|
||||
|
||||
public_turn = _function(sql, "conversational_rectification_valid_public_turn")
|
||||
for key in ("caseid", "journeyprotocol", "status"):
|
||||
assert f"jsonb_typeof(p_value -> '{key}') = 'string'" in public_turn
|
||||
|
||||
|
||||
def test_sql_uuid_text_matches_zod_canonical_hyphenated_syntax_at_all_json_boundaries() -> None:
|
||||
sql = _normalized(SCHEMA)
|
||||
uuid = _function(sql, "conversational_rectification_valid_uuid_text")
|
||||
assert "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" in uuid
|
||||
assert "perform p_value::uuid" in uuid
|
||||
|
||||
for validator in (
|
||||
"conversational_rectification_valid_evidence_recap",
|
||||
"conversational_rectification_valid_life_event_evidence",
|
||||
"conversational_rectification_valid_private_candidate",
|
||||
"conversational_rectification_valid_public_turn",
|
||||
"conversational_rectification_valid_action_request",
|
||||
"conversational_rectification_valid_action_response",
|
||||
):
|
||||
assert "conversational_rectification_valid_uuid_text" in _function(sql, validator)
|
||||
|
||||
|
||||
def test_sql_nonblank_and_maximum_text_rules_match_ecmascript_and_utf16() -> None:
|
||||
sql = _normalized(SCHEMA)
|
||||
assert "create or replace function public.conversational_rectification_text_utf16_length" in sql
|
||||
|
||||
for validator, minimum_nonblank_calls, minimum_utf16_calls in (
|
||||
("conversational_rectification_text_array_is_bounded", 1, 1),
|
||||
("conversational_rectification_valid_technical_receipt", 1, 1),
|
||||
("conversational_rectification_valid_evidence_recap", 2, 2),
|
||||
("conversational_rectification_valid_validation_receipt", 2, 3),
|
||||
("conversational_rectification_valid_life_event_evidence", 3, 3),
|
||||
("conversational_rectification_valid_private_candidate", 2, 2),
|
||||
("conversational_rectification_valid_declared_birth_input", 2, 2),
|
||||
("conversational_rectification_valid_public_turn", 2, 2),
|
||||
("conversational_rectification_valid_action_response", 2, 2),
|
||||
):
|
||||
body = _function(sql, validator)
|
||||
assert body.count("conversational_rectification_text_is_nonblank") >= minimum_nonblank_calls
|
||||
assert body.count("conversational_rectification_text_utf16_length") >= minimum_utf16_calls
|
||||
|
||||
for durable_column in ("narrative", "raw_text", "event_summary", "date_value"):
|
||||
assert f"conversational_rectification_text_utf16_length({durable_column})" in sql
|
||||
|
||||
|
||||
def test_life_event_evidence_is_strictly_validated_before_insert() -> None:
|
||||
schema = _normalized(SCHEMA)
|
||||
transitions = _normalized(TRANSITIONS)
|
||||
|
||||
@@ -638,6 +638,7 @@ def test_postgres_uses_the_shared_stable_numeric_boundary_vectors(
|
||||
stable_vector = {
|
||||
"zero": 0,
|
||||
"minFraction": 0.000001,
|
||||
"ieeeRoundingBoundary": 1.000001,
|
||||
"decimal": 0.123456,
|
||||
"maxSafe": 9_007_199_254_740_991,
|
||||
}
|
||||
@@ -649,11 +650,14 @@ def test_postgres_uses_the_shared_stable_numeric_boundary_vectors(
|
||||
)::text;
|
||||
"""
|
||||
)
|
||||
assert json.loads(numeric_result) == {"valid": True, "bytes": 86}
|
||||
assert json.loads(numeric_result) == {"valid": True, "bytes": 120}
|
||||
|
||||
for invalid in (
|
||||
{"nested": [{"score": 1e-7}]},
|
||||
{"nested": [{"score": 1e-100}]},
|
||||
{"nested": [{"score": 0.0000012}]},
|
||||
{"nested": [{"score": 0.1234567}]},
|
||||
{"nested": [{"score": 1_000_000.000001}]},
|
||||
{"nested": [{"score": 9_007_199_254_740_992}]},
|
||||
):
|
||||
assert pg14_database.sql(
|
||||
@@ -678,6 +682,123 @@ def test_postgres_uses_the_shared_stable_numeric_boundary_vectors(
|
||||
) == "false"
|
||||
|
||||
|
||||
def test_json_text_validators_align_types_uuid_unicode_and_utf16(
|
||||
pg14_database: PgDatabase,
|
||||
) -> None:
|
||||
case_id = "00000000-0000-4000-8000-000000000960"
|
||||
turn = _valid_turn(case_id)
|
||||
private_candidate = _valid_private_candidate()
|
||||
unicode_whitespace = "\u00a0\u2007\ufeff"
|
||||
canonical_uuid = "a9890e09-d535-46f0-9a36-86017515a5a1"
|
||||
compact_uuid = canonical_uuid.replace("-", "")
|
||||
|
||||
uuid_result = json.loads(pg14_database.sql(
|
||||
f"""
|
||||
select pg_catalog.jsonb_build_object(
|
||||
'lower', public.conversational_rectification_valid_uuid_text({_text(canonical_uuid)}),
|
||||
'upper', public.conversational_rectification_valid_uuid_text({_text(canonical_uuid.upper())}),
|
||||
'compact', public.conversational_rectification_valid_uuid_text({_text(compact_uuid)}),
|
||||
'braced', public.conversational_rectification_valid_uuid_text({_text('{' + canonical_uuid + '}')})
|
||||
)::text;
|
||||
"""
|
||||
))
|
||||
assert uuid_result == {"lower": True, "upper": True, "compact": False, "braced": False}
|
||||
|
||||
invalid_turns = (
|
||||
{**turn, "candidate": {**turn["candidate"], "status": None}},
|
||||
{
|
||||
**turn,
|
||||
"technicalReceipt": {**turn["technicalReceipt"], "stableLayers": [None]},
|
||||
},
|
||||
{
|
||||
**turn,
|
||||
"technicalReceipt": {**turn["technicalReceipt"], "sensitiveLayers": [42]},
|
||||
},
|
||||
{
|
||||
**turn,
|
||||
"evidenceRequest": {**turn["evidenceRequest"], "domains": ["career", None]},
|
||||
},
|
||||
{
|
||||
**turn,
|
||||
"evidenceRequest": {**turn["evidenceRequest"], "datePrecision": None},
|
||||
},
|
||||
{**turn, "actions": [None]},
|
||||
{**turn, "status": None},
|
||||
{**turn, "narrative": unicode_whitespace},
|
||||
{
|
||||
**turn,
|
||||
"evidenceRecap": [{
|
||||
"id": compact_uuid,
|
||||
"summary": "summary",
|
||||
"dateLabel": "2020-01",
|
||||
}],
|
||||
},
|
||||
)
|
||||
for invalid in invalid_turns:
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_valid_public_turn("
|
||||
f"{_jsonb(invalid)})::text"
|
||||
) == "false"
|
||||
|
||||
invalid_private_candidates = (
|
||||
{**private_candidate, "resultId": compact_uuid},
|
||||
{**private_candidate, "suggestedDomains": ["career", None]},
|
||||
{**private_candidate, "d1Stability": None},
|
||||
{**private_candidate, "calculationVersion": unicode_whitespace},
|
||||
{
|
||||
**private_candidate,
|
||||
"workingState": {"phase": "initial", "iteration": 0, "notes": [unicode_whitespace]},
|
||||
},
|
||||
{
|
||||
**private_candidate,
|
||||
"futureWindows": [{
|
||||
"label": unicode_whitespace,
|
||||
"startDate": "2020-01-01",
|
||||
"endDate": "2020-01-02",
|
||||
"scoreable": False,
|
||||
}],
|
||||
},
|
||||
)
|
||||
for invalid in invalid_private_candidates:
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_valid_private_candidate("
|
||||
f"{_jsonb(invalid)})::text"
|
||||
) == "false"
|
||||
|
||||
invalid_declared = {
|
||||
**_valid_declared_birth_input(),
|
||||
"birthTimeClue": unicode_whitespace,
|
||||
"birthplace": {
|
||||
**_valid_declared_birth_input()["birthplace"],
|
||||
"city": unicode_whitespace,
|
||||
},
|
||||
}
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_valid_declared_birth_input("
|
||||
f"{_jsonb(invalid_declared)})::text"
|
||||
) == "false"
|
||||
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_valid_validation_receipt("
|
||||
f"{_jsonb({'modelId': unicode_whitespace, 'schemaValidated': True})})::text"
|
||||
) == "false"
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_valid_action_response("
|
||||
f"{_jsonb({'success': False, 'credits': 7, 'billing_state': None, 'error_code': unicode_whitespace})}, "
|
||||
"'reserve_fee')::text"
|
||||
) == "false"
|
||||
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_text_utf16_length("
|
||||
f"{_text('😀' * 40)})::text"
|
||||
) == "80"
|
||||
for model_id, expected in (("😀" * 60, "true"), ("😀" * 61, "false")):
|
||||
assert pg14_database.sql(
|
||||
"select public.conversational_rectification_valid_validation_receipt("
|
||||
f"{_jsonb({'modelId': model_id, 'schemaValidated': True})})::text"
|
||||
) == expected
|
||||
|
||||
|
||||
def test_save_rejects_invalid_evidence_without_discarding_or_coercing_fields(
|
||||
pg14_database: PgDatabase,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user