Add AI-assisted world and character drafting flows
All checks were successful
CI / validate (push) Successful in 14m3s

This commit is contained in:
2026-08-17 09:25:37 +05:00
parent 830aa5dc1f
commit cdeea8653c
23 changed files with 811 additions and 92 deletions

View File

@@ -2030,6 +2030,151 @@ grant execute on function public.dng_schema_version() to service_role;
notify pgrst, 'reload schema';
-- ============================================================================
-- 0007_stage_three_four_completion.sql
-- ============================================================================
-- Complete the universe/character alpha slice: resumable coauthor questions and
-- an atomic, server-only character creation path.
alter table public.coauthor_sessions
add column if not exists current_question jsonb;
alter table public.coauthor_sessions
drop constraint if exists coauthor_sessions_current_question_object;
alter table public.coauthor_sessions
add constraint coauthor_sessions_current_question_object check (
current_question is null or jsonb_typeof(current_question) = 'object'
);
create or replace function public.stage_two_append_coauthor_message(
p_session_id uuid,
p_owner_id uuid,
p_content text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_session public.coauthor_sessions%rowtype;
v_result jsonb;
begin
if p_content is null or char_length(btrim(p_content)) not between 1 and 5000 then
raise exception 'message must be between 1 and 5000 characters';
end if;
select * into v_session from public.coauthor_sessions
where id = p_session_id and owner_id = p_owner_id for update;
if not found then raise exception 'coauthor session not found'; end if;
if v_session.status in ('generating', 'confirmed') then
raise exception 'coauthor session cannot be edited in status %', v_session.status;
end if;
update public.coauthor_sessions
set messages = messages || jsonb_build_array(jsonb_build_object('role', 'user', 'content', btrim(p_content))),
generated_world = null,
current_question = null,
status = 'collecting',
updated_at = now()
where id = p_session_id
returning jsonb_build_object('id', id, 'status', status, 'messages', messages, 'updatedAt', updated_at)
into v_result;
return v_result;
end;
$$;
create or replace function public.stage_four_set_coauthor_question(
p_session_id uuid,
p_owner_id uuid,
p_question jsonb
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_result jsonb;
begin
if p_question is null
or jsonb_typeof(p_question) <> 'object'
or nullif(btrim(p_question->>'id'), '') is null
or char_length(btrim(p_question->>'label')) not between 5 and 300
or jsonb_typeof(p_question->'options') <> 'array'
or jsonb_array_length(p_question->'options') <> 3 then
raise exception 'invalid coauthor question';
end if;
update public.coauthor_sessions
set messages = messages || jsonb_build_array(jsonb_build_object(
'role', 'assistant', 'content', btrim(p_question->>'label')
)),
current_question = p_question,
updated_at = now()
where id = p_session_id and owner_id = p_owner_id and status = 'collecting'
returning current_question into v_result;
if not found then raise exception 'collecting coauthor session not found'; end if;
return v_result;
end;
$$;
create or replace function public.stage_four_create_character(
p_campaign_id uuid,
p_actor_id uuid,
p_controller text,
p_name text,
p_concept text,
p_abilities jsonb,
p_hp integer,
p_max_hp integer,
p_defense integer,
p_proficiency integer,
p_inventory jsonb,
p_statuses jsonb,
p_persona jsonb
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_character_id uuid;
v_user_id uuid;
begin
if p_controller not in ('human', 'ai') then raise exception 'invalid character controller'; end if;
if not exists (
select 1 from public.campaign_members
where campaign_id = p_campaign_id and user_id = p_actor_id and active
) then raise exception 'active campaign membership is required'; end if;
if p_controller = 'ai' then
if not exists (
select 1 from public.campaigns where id = p_campaign_id and owner_id = p_actor_id
) then raise exception 'only the campaign owner can add AI heroes'; end if;
v_user_id := null;
else
v_user_id := p_actor_id;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(p_campaign_id::text || ':' || p_actor_id::text, 0)
);
if exists (
select 1 from public.characters
where campaign_id = p_campaign_id and user_id = p_actor_id
) then raise exception 'this player already has a character'; end if;
end if;
insert into public.characters(
campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
defense, proficiency, inventory, statuses, persona
) values (
p_campaign_id, v_user_id, btrim(p_name), btrim(p_concept),
p_controller::public.character_controller, p_abilities, p_hp, p_max_hp,
p_defense, p_proficiency, p_inventory, p_statuses, p_persona
) returning id into v_character_id;
return v_character_id;
end;
$$;
create or replace function public.dng_schema_version()
returns integer language sql stable security definer set search_path = '' as $$
select 7;
$$;
revoke all on function public.stage_four_set_coauthor_question(uuid, uuid, jsonb) from public, anon, authenticated;
revoke all on function public.stage_four_create_character(uuid, uuid, text, text, text, jsonb, integer, integer, integer, integer, jsonb, jsonb, jsonb) from public, anon, authenticated;
revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.stage_four_set_coauthor_question(uuid, uuid, jsonb) to service_role;
grant execute on function public.stage_four_create_character(uuid, uuid, text, text, text, jsonb, integer, integer, integer, integer, jsonb, jsonb, jsonb) to service_role;
grant execute on function public.dng_schema_version() to service_role;
notify pgrst, 'reload schema';
do $$
begin
@@ -2051,6 +2196,12 @@ begin
if to_regprocedure('public.dng_schema_version()') is null then
raise exception 'D&G bootstrap verification failed: schema version RPC is missing';
end if;
if to_regprocedure('public.stage_four_create_character(uuid,uuid,text,text,text,jsonb,integer,integer,integer,integer,jsonb,jsonb,jsonb)') is null then
raise exception 'D&G bootstrap verification failed: character creation RPC is missing';
end if;
if public.dng_schema_version() <> 7 then
raise exception 'D&G bootstrap verification failed: unexpected schema version';
end if;
end;
$$;

View File

@@ -0,0 +1,140 @@
-- Complete the universe/character alpha slice: resumable coauthor questions and
-- an atomic, server-only character creation path.
alter table public.coauthor_sessions
add column if not exists current_question jsonb;
alter table public.coauthor_sessions
drop constraint if exists coauthor_sessions_current_question_object;
alter table public.coauthor_sessions
add constraint coauthor_sessions_current_question_object check (
current_question is null or jsonb_typeof(current_question) = 'object'
);
create or replace function public.stage_two_append_coauthor_message(
p_session_id uuid,
p_owner_id uuid,
p_content text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_session public.coauthor_sessions%rowtype;
v_result jsonb;
begin
if p_content is null or char_length(btrim(p_content)) not between 1 and 5000 then
raise exception 'message must be between 1 and 5000 characters';
end if;
select * into v_session from public.coauthor_sessions
where id = p_session_id and owner_id = p_owner_id for update;
if not found then raise exception 'coauthor session not found'; end if;
if v_session.status in ('generating', 'confirmed') then
raise exception 'coauthor session cannot be edited in status %', v_session.status;
end if;
update public.coauthor_sessions
set messages = messages || jsonb_build_array(jsonb_build_object('role', 'user', 'content', btrim(p_content))),
generated_world = null,
current_question = null,
status = 'collecting',
updated_at = now()
where id = p_session_id
returning jsonb_build_object('id', id, 'status', status, 'messages', messages, 'updatedAt', updated_at)
into v_result;
return v_result;
end;
$$;
create or replace function public.stage_four_set_coauthor_question(
p_session_id uuid,
p_owner_id uuid,
p_question jsonb
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_result jsonb;
begin
if p_question is null
or jsonb_typeof(p_question) <> 'object'
or nullif(btrim(p_question->>'id'), '') is null
or char_length(btrim(p_question->>'label')) not between 5 and 300
or jsonb_typeof(p_question->'options') <> 'array'
or jsonb_array_length(p_question->'options') <> 3 then
raise exception 'invalid coauthor question';
end if;
update public.coauthor_sessions
set messages = messages || jsonb_build_array(jsonb_build_object(
'role', 'assistant', 'content', btrim(p_question->>'label')
)),
current_question = p_question,
updated_at = now()
where id = p_session_id and owner_id = p_owner_id and status = 'collecting'
returning current_question into v_result;
if not found then raise exception 'collecting coauthor session not found'; end if;
return v_result;
end;
$$;
create or replace function public.stage_four_create_character(
p_campaign_id uuid,
p_actor_id uuid,
p_controller text,
p_name text,
p_concept text,
p_abilities jsonb,
p_hp integer,
p_max_hp integer,
p_defense integer,
p_proficiency integer,
p_inventory jsonb,
p_statuses jsonb,
p_persona jsonb
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_character_id uuid;
v_user_id uuid;
begin
if p_controller not in ('human', 'ai') then raise exception 'invalid character controller'; end if;
if not exists (
select 1 from public.campaign_members
where campaign_id = p_campaign_id and user_id = p_actor_id and active
) then raise exception 'active campaign membership is required'; end if;
if p_controller = 'ai' then
if not exists (
select 1 from public.campaigns where id = p_campaign_id and owner_id = p_actor_id
) then raise exception 'only the campaign owner can add AI heroes'; end if;
v_user_id := null;
else
v_user_id := p_actor_id;
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtextextended(p_campaign_id::text || ':' || p_actor_id::text, 0)
);
if exists (
select 1 from public.characters
where campaign_id = p_campaign_id and user_id = p_actor_id
) then raise exception 'this player already has a character'; end if;
end if;
insert into public.characters(
campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
defense, proficiency, inventory, statuses, persona
) values (
p_campaign_id, v_user_id, btrim(p_name), btrim(p_concept),
p_controller::public.character_controller, p_abilities, p_hp, p_max_hp,
p_defense, p_proficiency, p_inventory, p_statuses, p_persona
) returning id into v_character_id;
return v_character_id;
end;
$$;
create or replace function public.dng_schema_version()
returns integer language sql stable security definer set search_path = '' as $$
select 7;
$$;
revoke all on function public.stage_four_set_coauthor_question(uuid, uuid, jsonb) from public, anon, authenticated;
revoke all on function public.stage_four_create_character(uuid, uuid, text, text, text, jsonb, integer, integer, integer, integer, jsonb, jsonb, jsonb) from public, anon, authenticated;
revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.stage_four_set_coauthor_question(uuid, uuid, jsonb) to service_role;
grant execute on function public.stage_four_create_character(uuid, uuid, text, text, text, jsonb, integer, integer, integer, integer, jsonb, jsonb, jsonb) to service_role;
grant execute on function public.dng_schema_version() to service_role;
notify pgrst, 'reload schema';