-- Guarded KB review/apply prerequisites. Run as postgres at deploy time.
--
-- Authority is deliberately split:
--   * kb_review can read proposals and execute one constrained approval function;
--   * kb_apply can read approved proposals, perform narrow canonical writes, and
--     execute payload-bound assert/finish functions;
--   * kb_gate_owner is a NOLOGIN owner for gate tables/functions;
--   * neither login role can directly UPDATE kb_stage.kb_proposals or mutate the
--     immutable approval snapshot.
--
-- The deployer provisions passwords separately in kb-review.env/kb-apply.env.
-- This file contains no credentials and is safe to reuse in disposable clones.
-- Before the first V3 contract migration, run this file, stop the V2 worker,
-- then ALTER ROLE kb_apply NOLOGIN and end its sessions. After V3 is installed,
-- this file may re-enable the login: a V2-configured worker detects the cutover
-- and leaves historical V2 approvals review-visible until migrate/reject.

set timezone = 'UTC';

begin;

do $deployer$
begin
  if not exists (
    select 1
      from pg_catalog.pg_roles
     where rolname = current_user
       and rolsuper
  ) then
    raise exception 'kb_apply_prereqs: current role % must be a superuser', current_user;
  end if;
  if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_apply') then
    raise exception 'kb_apply_prereqs: required runtime role kb_apply does not exist';
  end if;
end
$deployer$;

do $roles$
begin
  if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_gate_owner') then
    create role kb_gate_owner nologin;
  end if;
  if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_review') then
    create role kb_review login;
  end if;
end
$roles$;

alter role kb_gate_owner nologin noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;
alter role kb_review login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;
alter role kb_apply login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;

-- The independently provisioned GCP runtime keeps an exact column-level read
-- projection. Record that reviewed contract in this transaction so a later
-- review/apply prerequisite rerun can preserve the scoped grants without
-- accepting arbitrary runtime ACLs. The matching test binds these rows to the
-- runtime-role provisioning manifest and the executable query contract.
create temporary table kb_apply_prereqs_leoclean_read_column_allowlist (
  schema_name name not null,
  relation_name name not null,
  column_name name not null,
  column_ordinal smallint not null,
  primary key (schema_name, relation_name, column_name),
  unique (schema_name, relation_name, column_ordinal)
) on commit drop;

insert into pg_temp.kb_apply_prereqs_leoclean_read_column_allowlist (
  schema_name,
  relation_name,
  column_name,
  column_ordinal
) values
  ('public', 'agents', 'id', 1),
  ('public', 'agents', 'handle', 2),
  ('public', 'claims', 'id', 1),
  ('public', 'claims', 'type', 2),
  ('public', 'claims', 'text', 3),
  ('public', 'claims', 'status', 4),
  ('public', 'claims', 'confidence', 5),
  ('public', 'claims', 'tags', 6),
  ('public', 'claims', 'superseded_by', 7),
  ('public', 'claims', 'created_at', 8),
  ('public', 'claims', 'updated_at', 9),
  ('public', 'claim_evidence', 'claim_id', 1),
  ('public', 'claim_evidence', 'source_id', 2),
  ('public', 'claim_evidence', 'role', 3),
  ('public', 'claim_evidence', 'weight', 4),
  ('public', 'claim_edges', 'id', 1),
  ('public', 'claim_edges', 'from_claim', 2),
  ('public', 'claim_edges', 'to_claim', 3),
  ('public', 'claim_edges', 'edge_type', 4),
  ('public', 'sources', 'id', 1),
  ('public', 'sources', 'source_type', 2),
  ('public', 'sources', 'url', 3),
  ('public', 'sources', 'storage_path', 4),
  ('public', 'sources', 'excerpt', 5),
  ('public', 'sources', 'hash', 6),
  ('public', 'personas', 'agent_id', 1),
  ('public', 'personas', 'name', 2),
  ('public', 'personas', 'voice', 3),
  ('public', 'personas', 'role', 4),
  ('public', 'personas', 'source_ref', 5),
  ('public', 'personas', 'lens', 6),
  ('public', 'strategies', 'agent_id', 1),
  ('public', 'strategies', 'diagnosis', 2),
  ('public', 'strategies', 'guiding_policy', 3),
  ('public', 'strategies', 'proximate_objectives', 4),
  ('public', 'strategies', 'version', 5),
  ('public', 'strategies', 'active', 6),
  ('public', 'beliefs', 'agent_id', 1),
  ('public', 'beliefs', 'level', 2),
  ('public', 'beliefs', 'statement', 3),
  ('public', 'beliefs', 'falsifier', 4),
  ('public', 'beliefs', 'rank', 5),
  ('public', 'beliefs', 'status', 6),
  ('public', 'blindspots', 'agent_id', 1),
  ('public', 'blindspots', 'name', 2),
  ('public', 'blindspots', 'description', 3),
  ('public', 'blindspots', 'correction', 4),
  ('public', 'blindspots', 'kind', 5),
  ('public', 'blindspots', 'status', 6),
  ('public', 'agent_roles', 'agent_id', 1),
  ('public', 'agent_roles', 'title', 2),
  ('public', 'agent_roles', 'description', 3),
  ('public', 'peer_models', 'subject_id', 1),
  ('public', 'peer_models', 'peer_id', 2),
  ('public', 'peer_models', 'domain', 3),
  ('public', 'peer_models', 'outranks_on', 4),
  ('public', 'peer_models', 'deference_rule', 5),
  ('public', 'behavioral_rules', 'agent_id', 1),
  ('public', 'behavioral_rules', 'category', 2),
  ('public', 'behavioral_rules', 'rule', 3),
  ('public', 'behavioral_rules', 'rationale', 4),
  ('public', 'contributor_rules', 'agent_id', 1),
  ('public', 'contributor_rules', 'name', 2),
  ('public', 'contributor_rules', 'directive', 3),
  ('public', 'contributor_rules', 'ci_tier', 4),
  ('public', 'contributor_rules', 'weighting', 5),
  ('public', 'contributor_rules', 'rationale', 6),
  ('public', 'reasoning_tools', 'agent_id', 1),
  ('public', 'reasoning_tools', 'name', 2),
  ('public', 'reasoning_tools', 'description', 3),
  ('public', 'reasoning_tools', 'category', 4),
  ('public', 'governance_gates', 'agent_id', 1),
  ('public', 'governance_gates', 'name', 2),
  ('public', 'governance_gates', 'criteria', 3),
  ('public', 'governance_gates', 'evidence_bar', 4),
  ('public', 'governance_gates', 'pass_condition', 5),
  ('kb_stage', 'kb_proposals', 'id', 1),
  ('kb_stage', 'kb_proposals', 'proposal_type', 2),
  ('kb_stage', 'kb_proposals', 'status', 3),
  ('kb_stage', 'kb_proposals', 'proposed_by_handle', 4),
  ('kb_stage', 'kb_proposals', 'proposed_by_agent_id', 5),
  ('kb_stage', 'kb_proposals', 'channel', 6),
  ('kb_stage', 'kb_proposals', 'source_ref', 7),
  ('kb_stage', 'kb_proposals', 'rationale', 8),
  ('kb_stage', 'kb_proposals', 'payload', 9),
  ('kb_stage', 'kb_proposals', 'reviewed_by_handle', 10),
  ('kb_stage', 'kb_proposals', 'reviewed_at', 11),
  ('kb_stage', 'kb_proposals', 'review_note', 12),
  ('kb_stage', 'kb_proposals', 'applied_by_handle', 13),
  ('kb_stage', 'kb_proposals', 'applied_at', 14),
  ('kb_stage', 'kb_proposals', 'created_at', 15),
  ('kb_stage', 'kb_proposals', 'updated_at', 16);

-- The proposal function owner has only the exact underlying table rights its
-- SECURITY DEFINER implementation needs. These rows describe bounded ACLs that
-- may already exist when this prerequisite is rerun after runtime provisioning.
create temporary table kb_apply_prereqs_leoclean_stage_owner_column_allowlist (
  schema_name name not null,
  relation_name name not null,
  column_name name not null,
  privilege_type text not null,
  primary key (schema_name, relation_name, column_name, privilege_type)
) on commit drop;

insert into pg_temp.kb_apply_prereqs_leoclean_stage_owner_column_allowlist (
  schema_name,
  relation_name,
  column_name,
  privilege_type
) values
  ('public', 'agents', 'id', 'SELECT'),
  ('public', 'agents', 'handle', 'SELECT'),
  ('kb_stage', 'kb_proposals', 'proposal_type', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'status', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'proposed_by_handle', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'proposed_by_agent_id', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'channel', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'source_ref', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'rationale', 'INSERT'),
  ('kb_stage', 'kb_proposals', 'payload', 'INSERT');

-- Snapshot the exact bounded external ACLs present before normalization. Final
-- verification compares against these rows, so this migration must preserve
-- them exactly; it cannot silently add another otherwise allowlisted grant.
create temporary table kb_apply_prereqs_preserved_table_acls (
  schema_name name not null,
  relation_name name not null,
  grantee name not null,
  privilege_type text not null,
  is_grantable boolean not null,
  primary key (schema_name, relation_name, grantee, privilege_type)
) on commit drop;

insert into pg_temp.kb_apply_prereqs_preserved_table_acls (
  schema_name,
  relation_name,
  grantee,
  privilege_type,
  is_grantable
)
select namespace.nspname,
       relation.relname,
       grantee_role.rolname,
       pg_catalog.upper(acl.privilege_type),
       acl.is_grantable
  from pg_catalog.pg_class relation
  join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
 cross join lateral pg_catalog.aclexplode(
   coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
 ) acl
  join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
 where (
   grantee_role.rolname = 'kb_observatory_read'
   and (namespace.nspname, relation.relname) in (
     ('public', 'claims'),
     ('public', 'sources'),
     ('public', 'claim_evidence'),
     ('public', 'claim_edges'),
     ('kb_stage', 'kb_proposals')
   )
   and pg_catalog.upper(acl.privilege_type) = 'SELECT'
   and not acl.is_grantable
 ) or (
   grantee_role.rolname = 'leoclean_kb_stage_owner'
   and namespace.nspname = 'kb_stage'
   and relation.relname = 'kb_proposals'
   and pg_catalog.upper(acl.privilege_type) = 'SELECT'
   and not acl.is_grantable
 );

create temporary table kb_apply_prereqs_preserved_column_acls (
  schema_name name not null,
  relation_name name not null,
  column_name name not null,
  grantee name not null,
  privilege_type text not null,
  is_grantable boolean not null,
  primary key (schema_name, relation_name, column_name, grantee, privilege_type)
) on commit drop;

insert into pg_temp.kb_apply_prereqs_preserved_column_acls (
  schema_name,
  relation_name,
  column_name,
  grantee,
  privilege_type,
  is_grantable
)
select namespace.nspname,
       relation.relname,
       attribute.attname,
       grantee_role.rolname,
       pg_catalog.upper(acl.privilege_type),
       acl.is_grantable
  from pg_catalog.pg_attribute attribute
  join pg_catalog.pg_class relation on relation.oid = attribute.attrelid
  join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
 cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
  join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
 where (namespace.nspname, relation.relname) in (
         ('public', 'agents'),
         ('public', 'strategies'),
         ('public', 'strategy_nodes'),
         ('public', 'claim_evidence'),
         ('public', 'claim_evidence_assessments'),
         ('public', 'claim_edges'),
         ('public', 'claims'),
         ('public', 'sources'),
         ('public', 'reasoning_tools'),
         ('kb_stage', 'kb_proposals'),
         ('kb_stage', 'kb_review_principals'),
         ('kb_stage', 'kb_proposal_approvals'),
         ('kb_stage', 'teleo_v3_installation_ownership')
       )
   and attribute.attnum > 0
   and not attribute.attisdropped
   and not acl.is_grantable
   and (
     (
       grantee_role.rolname = 'leoclean_kb_runtime'
       and pg_catalog.upper(acl.privilege_type) = 'SELECT'
       and exists (
         select 1
           from pg_temp.kb_apply_prereqs_leoclean_read_column_allowlist allowlist
          where allowlist.schema_name = namespace.nspname
            and allowlist.relation_name = relation.relname
            and allowlist.column_name = attribute.attname
       )
     )
     or (
       grantee_role.rolname = 'leoclean_kb_stage_owner'
       and exists (
         select 1
           from pg_temp.kb_apply_prereqs_leoclean_stage_owner_column_allowlist allowlist
          where allowlist.schema_name = namespace.nspname
            and allowlist.relation_name = relation.relname
            and allowlist.column_name = attribute.attname
            and allowlist.privilege_type = pg_catalog.upper(acl.privilege_type)
       )
     )
   );

-- Protected review/apply roles are standalone principals. The Observatory
-- service principal is intentionally a member of kb_observatory_read, but the
-- read role itself must never be a member of another role: that would create an
-- inherited or SET ROLE path beyond its bounded SELECT surface.
do $memberships$
declare
  v_memberships text;
begin
  select pg_catalog.string_agg(
           pg_catalog.format('%I -> %I', member_role.rolname, granted_role.rolname),
           ', ' order by member_role.rolname, granted_role.rolname
         )
    into v_memberships
    from pg_catalog.pg_auth_members membership
    join pg_catalog.pg_roles member_role on member_role.oid = membership.member
    join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
   where member_role.rolname in (
           'kb_gate_owner', 'kb_review', 'kb_apply', 'kb_observatory_read',
           'leoclean_kb_runtime', 'leoclean_kb_stage_owner'
         )
      or granted_role.rolname in (
           'kb_gate_owner', 'kb_review', 'kb_apply',
           'leoclean_kb_runtime', 'leoclean_kb_stage_owner'
         );

  if v_memberships is not null then
    raise exception 'kb_apply_prereqs: unexpected protected role membership: %', v_memberships;
  end if;
end
$memberships$;

-- Service identity used for the applied_by_agent_id foreign key. kb_apply can
-- read this row but can never create or modify agent rows.
insert into public.agents (id, handle, kind)
values ('44444444-4444-4444-4444-444444444444', 'kb-apply', 'service')
on conflict (handle) do nothing;

-- Canonicalize the existing human reviewer identity without changing its UUID.
-- Historical proposal/approval reviewer text is deliberately left untouched.
do $reviewer_identity$
declare
  v_legacy_id uuid;
  v_canonical_id uuid;
begin
  select id into v_legacy_id
    from public.agents
   where handle = 'm3ta'
   for update;

  select id into v_canonical_id
    from public.agents
   where handle = 'm3taversal'
   for update;

  if v_legacy_id is not null
     and v_canonical_id is not null
     and v_legacy_id <> v_canonical_id then
    raise exception
      'kb_apply_prereqs: both reviewer handles m3ta (%) and m3taversal (%) exist with different agent UUIDs; resolve the identity conflict before rerunning',
      v_legacy_id, v_canonical_id;
  end if;

  if v_canonical_id is null and v_legacy_id is not null then
    update public.agents
       set handle = 'm3taversal'
     where id = v_legacy_id
       and handle = 'm3ta';
    if not found then
      raise exception
        'kb_apply_prereqs: legacy reviewer m3ta changed during canonical identity migration';
    end if;
    v_canonical_id := v_legacy_id;
  end if;

  if v_canonical_id is null then
    raise exception
      'kb_apply_prereqs: required human reviewer agent is missing; restore exactly one public.agents row with handle m3ta or m3taversal before rerunning';
  end if;
end
$reviewer_identity$;

-- DB-login to human-reviewer mapping. Only the gate owner/admin can change it;
-- the approval function verifies --reviewed-by against this mapping rather than
-- trusting caller-supplied identity. The dedicated reviewer credential always
-- maps to the canonical m3taversal human agent.
create table if not exists kb_stage.kb_review_principals (
  db_role name primary key,
  reviewed_by_handle text not null,
  reviewed_by_agent_id uuid not null references public.agents(id),
  active boolean not null default true,
  created_at timestamptz not null default now()
);

-- One immutable snapshot per reviewed proposal. Apply is bound to this row as
-- well as the current proposal ledger, so an approved payload cannot drift.
create table if not exists kb_stage.kb_proposal_approvals (
  proposal_id uuid primary key references kb_stage.kb_proposals(id),
  proposal_type text not null,
  payload jsonb not null,
  reviewed_by_handle text not null,
  reviewed_by_agent_id uuid not null references public.agents(id),
  reviewed_by_db_role name not null,
  reviewed_at timestamptz not null,
  review_note text not null,
  approved_proposal_snapshot jsonb
);

-- Existing canonical/proposal tables must remain owned by the deployer. Gate
-- tables may be owned by the deployer during a legacy upgrade or kb_gate_owner
-- after an earlier guarded run. Anything else is ownership drift.
do $gate_preflight$
declare
  v_drift text;
begin
  select pg_catalog.string_agg(
           pg_catalog.format('%I.%I owned by %I', namespace.nspname, relation.relname, owner_role.rolname),
           ', ' order by namespace.nspname, relation.relname
         )
    into v_drift
    from pg_catalog.pg_class relation
    join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
   where (namespace.nspname, relation.relname) in (
           ('public', 'agents'),
           ('public', 'strategies'),
           ('public', 'strategy_nodes'),
           ('public', 'claim_evidence'),
           ('public', 'claim_evidence_assessments'),
           ('public', 'claim_edges'),
           ('public', 'claims'),
           ('public', 'sources'),
           ('public', 'reasoning_tools'),
           ('kb_stage', 'kb_proposals'),
           ('kb_stage', 'kb_review_principals'),
           ('kb_stage', 'kb_proposal_approvals'),
           ('kb_stage', 'teleo_v3_installation_ownership')
         )
     and relation.relkind in ('r', 'p')
     and not (
       (namespace.nspname = 'kb_stage'
        and relation.relname in (
          'kb_review_principals', 'kb_proposal_approvals', 'teleo_v3_installation_ownership'
        )
        and owner_role.rolname in (current_user, 'kb_gate_owner'))
       or (not (
             namespace.nspname = 'kb_stage'
             and relation.relname in (
               'kb_review_principals', 'kb_proposal_approvals', 'teleo_v3_installation_ownership'
             )
           )
           and owner_role.rolname = current_user)
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: unexpected protected table owner: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I.%I(%s) owned by %I',
             namespace.nspname,
             procedure.proname,
             pg_catalog.oidvectortypes(procedure.proargtypes),
             owner_role.rolname
           ),
           ', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes)
         )
    into v_drift
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
   where namespace.nspname = 'kb_stage'
     and procedure.proname in (
           'approve_strict_proposal',
           'assert_approved_proposal',
           'finish_approved_proposal',
           'export_applied_proposal_replay_rows'
         )
     and owner_role.rolname not in (current_user, 'kb_gate_owner');

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: unexpected protected function owner: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I.%I(%s)',
             namespace.nspname,
             procedure.proname,
             pg_catalog.oidvectortypes(procedure.proargtypes)
           ),
           ', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes)
         )
    into v_drift
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
   where namespace.nspname = 'kb_stage'
     and procedure.proname in (
           'approve_strict_proposal',
           'assert_approved_proposal',
           'finish_approved_proposal',
           'export_applied_proposal_replay_rows'
         )
     and not (
       (procedure.proname = 'approve_strict_proposal'
        and pg_catalog.oidvectortypes(procedure.proargtypes) in (
          'uuid, text, text',
          'uuid, text, jsonb, text, text'
        ))
       or (procedure.proname = 'assert_approved_proposal'
           and pg_catalog.oidvectortypes(procedure.proargtypes) =
             'uuid, text, jsonb, text, uuid, timestamp with time zone, text')
       or (procedure.proname = 'finish_approved_proposal'
           and pg_catalog.oidvectortypes(procedure.proargtypes) =
             'uuid, text, jsonb, text, uuid, timestamp with time zone, text, text')
       or (procedure.proname = 'export_applied_proposal_replay_rows'
           and pg_catalog.oidvectortypes(procedure.proargtypes) = 'uuid')
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: unexpected protected function overload: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I.%I -> %s',
             namespace.nspname,
             relation.relname,
             case
               when acl.grantee = 0 then 'PUBLIC'
               else pg_catalog.pg_get_userbyid(acl.grantee)
             end
           ),
           ', ' order by namespace.nspname, relation.relname, acl.grantee
         )
    into v_drift
    from pg_catalog.pg_class relation
    join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
    cross join lateral pg_catalog.aclexplode(
      coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
    ) acl
   where (namespace.nspname, relation.relname) in (
           ('public', 'agents'),
           ('public', 'strategies'),
           ('public', 'strategy_nodes'),
           ('public', 'claim_evidence'),
           ('public', 'claim_evidence_assessments'),
           ('public', 'claim_edges'),
           ('public', 'claims'),
           ('public', 'sources'),
           ('public', 'reasoning_tools'),
           ('kb_stage', 'kb_proposals'),
           ('kb_stage', 'kb_review_principals'),
           ('kb_stage', 'kb_proposal_approvals'),
           ('kb_stage', 'teleo_v3_installation_ownership')
         )
     and acl.grantee <> 0
     and acl.grantee <> relation.relowner
     and not (
       pg_catalog.pg_get_userbyid(acl.grantee) in (
         'kb_gate_owner', 'kb_review', 'kb_apply'
       )
       or exists (
         select 1
           from pg_temp.kb_apply_prereqs_preserved_table_acls preserved
          where preserved.schema_name = namespace.nspname
            and preserved.relation_name = relation.relname
            and preserved.grantee = pg_catalog.pg_get_userbyid(acl.grantee)
            and preserved.privilege_type = pg_catalog.upper(acl.privilege_type)
            and preserved.is_grantable = acl.is_grantable
       )
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: unexpected protected table grantee: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I.%I.%I -> %s',
             namespace.nspname,
             relation.relname,
             attribute.attname,
             case
               when acl.grantee = 0 then 'PUBLIC'
               else pg_catalog.pg_get_userbyid(acl.grantee)
             end
           ),
           ', ' order by namespace.nspname, relation.relname, attribute.attnum, acl.grantee
         )
    into v_drift
    from pg_catalog.pg_attribute attribute
    join pg_catalog.pg_class relation on relation.oid = attribute.attrelid
    join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
    cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
   where (namespace.nspname, relation.relname) in (
           ('public', 'agents'),
           ('public', 'strategies'),
           ('public', 'strategy_nodes'),
           ('public', 'claim_evidence'),
           ('public', 'claim_evidence_assessments'),
           ('public', 'claim_edges'),
           ('public', 'claims'),
           ('public', 'sources'),
           ('public', 'reasoning_tools'),
           ('kb_stage', 'kb_proposals'),
           ('kb_stage', 'kb_review_principals'),
           ('kb_stage', 'kb_proposal_approvals'),
           ('kb_stage', 'teleo_v3_installation_ownership')
         )
     and not attribute.attisdropped
     and acl.grantee <> 0
     and acl.grantee <> relation.relowner
     and not (
       pg_catalog.pg_get_userbyid(acl.grantee) in (
         'kb_gate_owner', 'kb_review', 'kb_apply'
       )
       or exists (
         select 1
           from pg_temp.kb_apply_prereqs_preserved_column_acls preserved
          where preserved.schema_name = namespace.nspname
            and preserved.relation_name = relation.relname
            and preserved.column_name = attribute.attname
            and preserved.grantee = pg_catalog.pg_get_userbyid(acl.grantee)
            and preserved.privilege_type = pg_catalog.upper(acl.privilege_type)
            and preserved.is_grantable = acl.is_grantable
       )
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: unexpected protected table grantee: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I.%I(%s) -> %s',
             namespace.nspname,
             procedure.proname,
             pg_catalog.oidvectortypes(procedure.proargtypes),
             case
               when acl.grantee = 0 then 'PUBLIC'
               else pg_catalog.pg_get_userbyid(acl.grantee)
             end
           ),
           ', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes), acl.grantee
         )
    into v_drift
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
    cross join lateral pg_catalog.aclexplode(
      coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
    ) acl
   where namespace.nspname = 'kb_stage'
     and procedure.proname in (
           'approve_strict_proposal',
           'assert_approved_proposal',
           'finish_approved_proposal',
           'export_applied_proposal_replay_rows'
         )
     and acl.grantee <> 0
     and acl.grantee <> procedure.proowner
     and pg_catalog.pg_get_userbyid(acl.grantee) not in (
           'kb_gate_owner', 'kb_review', 'kb_apply'
         );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: unexpected protected function grantee: %', v_drift;
  end if;
end
$gate_preflight$;

-- This legacy three-argument SECURITY DEFINER path trusted caller-supplied
-- reviewer text. Its exact signature is the only obsolete overload auto-removed.
drop function if exists kb_stage.approve_strict_proposal(uuid, text, text);

alter table kb_stage.kb_review_principals owner to kb_gate_owner;
alter table kb_stage.kb_proposal_approvals owner to kb_gate_owner;

alter table kb_stage.kb_proposal_approvals
  add column if not exists approved_proposal_snapshot jsonb;

comment on column kb_stage.kb_proposal_approvals.approved_proposal_snapshot is
  'Exact post-approval kb_proposals row. NULL means exact replay material is unavailable.';

-- A still-approved legacy row is the only state whose complete approved shape
-- remains recoverable without guessing. Already-applied rows stay NULL because
-- their pre-apply fields, especially updated_at, cannot be reconstructed.
update kb_stage.kb_proposal_approvals approval
   set approved_proposal_snapshot = pg_catalog.to_jsonb(proposal)
  from kb_stage.kb_proposals proposal
 where approval.proposal_id = proposal.id
   and approval.approved_proposal_snapshot is null
   and proposal.status = 'approved'
   and proposal.applied_by_handle is null
   and proposal.applied_by_agent_id is null
   and proposal.applied_at is null
   and approval.proposal_type = proposal.proposal_type
   and approval.payload = proposal.payload
   and approval.reviewed_by_handle = proposal.reviewed_by_handle
   and approval.reviewed_by_agent_id is not distinct from proposal.reviewed_by_agent_id
   and approval.reviewed_at = proposal.reviewed_at
   and approval.review_note = proposal.review_note;

insert into kb_stage.kb_review_principals
  (db_role, reviewed_by_handle, reviewed_by_agent_id, active, created_at)
select 'kb_review'::name, 'm3taversal', a.id, true,
       '1970-01-01 00:00:00+00'::timestamptz
  from public.agents a
 where a.handle = 'm3taversal'
on conflict (db_role) do update
  set reviewed_by_handle = excluded.reviewed_by_handle,
      reviewed_by_agent_id = excluded.reviewed_by_agent_id,
      active = excluded.active;

-- Reset every explicit privilege held by the protected roles before granting
-- the documented matrix. This removes stale DELETE/TRUNCATE/REFERENCES/TRIGGER
-- rights from upgrades instead of layering narrow grants on top of them.
revoke all privileges on schema public, kb_stage
  from kb_gate_owner, kb_apply, kb_review;
grant usage on schema public, kb_stage to kb_gate_owner;
grant usage on schema public, kb_stage to kb_apply;
grant usage on schema public, kb_stage to kb_review;

revoke all privileges on table
  public.agents,
  public.strategies,
  public.strategy_nodes,
  public.claim_evidence,
  public.claim_edges,
  public.claims,
  public.sources,
  public.reasoning_tools,
  kb_stage.kb_proposals,
  kb_stage.kb_review_principals,
  kb_stage.kb_proposal_approvals
  from kb_gate_owner, kb_apply, kb_review;
revoke all privileges on table
  kb_stage.kb_proposals,
  kb_stage.kb_review_principals,
  kb_stage.kb_proposal_approvals
  from public;

-- Column grants are stored separately from table ACLs and survive a table-level
-- REVOKE. Clear them explicitly so stale per-column writes cannot bypass the
-- normalized table matrix.
do $column_acl_reset$
declare
  protected_table record;
begin
  for protected_table in
    select namespace.nspname as schema_name,
           relation.relname as table_name,
           pg_catalog.string_agg(
             pg_catalog.format('%I', attribute.attname),
             ', ' order by attribute.attnum
           ) as column_list
      from pg_catalog.pg_class relation
      join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
      join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
     where (namespace.nspname, relation.relname) in (
             ('public', 'agents'),
             ('public', 'strategies'),
             ('public', 'strategy_nodes'),
             ('public', 'claim_evidence'),
             ('public', 'claim_evidence_assessments'),
             ('public', 'claim_edges'),
             ('public', 'claims'),
             ('public', 'sources'),
             ('public', 'reasoning_tools'),
             ('kb_stage', 'kb_proposals'),
             ('kb_stage', 'kb_review_principals'),
             ('kb_stage', 'kb_proposal_approvals'),
             ('kb_stage', 'teleo_v3_installation_ownership')
           )
       and attribute.attnum > 0
       and not attribute.attisdropped
     group by namespace.nspname, relation.relname
  loop
    execute pg_catalog.format(
      'revoke all privileges (%s) on table %I.%I from kb_gate_owner, kb_apply, kb_review',
      protected_table.column_list,
      protected_table.schema_name,
      protected_table.table_name
    );
    if protected_table.schema_name = 'kb_stage'
       and protected_table.table_name in (
         'kb_proposals', 'kb_review_principals', 'kb_proposal_approvals',
         'teleo_v3_installation_ownership'
       ) then
      execute pg_catalog.format(
        'revoke all privileges (%s) on table %I.%I from public',
        protected_table.column_list,
        protected_table.schema_name,
        protected_table.table_name
      );
    end if;
  end loop;
end
$column_acl_reset$;

grant select on public.agents to kb_gate_owner;
grant select on public.agents to kb_apply;
grant select on public.agents to kb_review;

grant select, insert, update on public.strategies to kb_apply;
grant select, insert, update on public.strategy_nodes to kb_apply;
grant select, insert on public.claim_evidence to kb_apply;
grant select, insert on public.claim_edges to kb_apply;
grant select, insert on public.claims to kb_apply;
grant select, insert on public.sources to kb_apply;
grant select, insert on public.reasoning_tools to kb_apply;

-- The V3 assessment table is installed by a separate additive migration. Keep
-- this prerequisite runnable before that migration, then normalize its ACLs on
-- the first post-migration rerun.
do $v3_assessment_acl$
declare
  v_columns text;
begin
  if pg_catalog.to_regclass('public.claim_evidence_assessments') is not null then
    execute 'revoke all privileges on table public.claim_evidence_assessments '
            'from public, kb_gate_owner, kb_apply, kb_review';
    select pg_catalog.string_agg(pg_catalog.format('%I', attribute.attname), ', ' order by attribute.attnum)
      into v_columns
      from pg_catalog.pg_attribute attribute
     where attribute.attrelid = 'public.claim_evidence_assessments'::regclass
       and attribute.attnum > 0
       and not attribute.attisdropped;
    if v_columns is not null then
      execute pg_catalog.format(
        'revoke all privileges (%s) on table public.claim_evidence_assessments '
        'from public, kb_gate_owner, kb_apply, kb_review',
        v_columns
      );
    end if;
    execute 'grant select, insert on public.claim_evidence_assessments to kb_apply';
    execute 'grant select on public.claims, public.sources, public.claim_evidence, '
            'public.claim_edges, public.claim_evidence_assessments to kb_gate_owner';
  end if;
end
$v3_assessment_acl$;

do $v3_installation_ownership_acl$
begin
  if pg_catalog.to_regclass('kb_stage.teleo_v3_installation_ownership') is not null then
    execute 'revoke all privileges on table kb_stage.teleo_v3_installation_ownership '
            'from public, kb_gate_owner, kb_apply, kb_review';
  end if;
end
$v3_installation_ownership_acl$;

-- Keep the primary ledger denial explicit in addition to the full reset above.
revoke update on kb_stage.kb_proposals from kb_apply;
grant select on kb_stage.kb_proposals to kb_apply;
grant select on kb_stage.kb_proposals to kb_review;

grant select, update on kb_stage.kb_proposals to kb_gate_owner;
grant select on kb_stage.kb_review_principals to kb_gate_owner;
grant select, insert on kb_stage.kb_proposal_approvals to kb_gate_owner;

-- Required conflict arbiters. Existing production constraints satisfy these
-- checks; fresh clones receive named indexes only when no equivalent unique
-- index exists. Migration failure on pre-existing duplicates is intentional.
do $indexes$
begin
  if not exists (
    select 1
      from pg_catalog.pg_index i
      join pg_catalog.pg_class t on t.oid = i.indrelid
      join pg_catalog.pg_namespace n on n.oid = t.relnamespace
     where n.nspname = 'public'
       and t.relname = 'sources'
       and i.indisunique
       and i.indpred is null
       and replace(pg_catalog.pg_get_indexdef(i.indexrelid), ' ', '') like '%(hash)%'
  ) then
    execute 'create unique index kb_apply_sources_hash_unique on public.sources (hash)';
  end if;

  if not exists (
    select 1
      from pg_catalog.pg_index i
      join pg_catalog.pg_class t on t.oid = i.indrelid
      join pg_catalog.pg_namespace n on n.oid = t.relnamespace
     where n.nspname = 'public'
       and t.relname = 'claim_evidence'
       and i.indisunique
       and i.indpred is null
       and replace(pg_catalog.pg_get_indexdef(i.indexrelid), ' ', '')
           like '%(claim_id,source_id,role)%'
  ) then
    execute 'create unique index kb_apply_claim_evidence_semantic_unique '
            'on public.claim_evidence (claim_id, source_id, role)';
  end if;
end
$indexes$;

create unique index if not exists one_active_strategy_per_agent
    on public.strategies (agent_id) where active;

-- Reviewer-only state transition. The expected type/payload are the exact
-- Python-validated snapshot; a concurrent pending-row mutation makes the
-- locked WHERE clause fail. Reviewer identity comes from session_user mapping.
create or replace function kb_stage.approve_strict_proposal(
  p_proposal_id uuid,
  p_expected_proposal_type text,
  p_expected_payload jsonb,
  p_expected_reviewed_by_handle text,
  p_review_note text
) returns jsonb
language plpgsql
security definer
set search_path = pg_catalog, pg_temp
set timezone = 'UTC'
as $function$
declare
  v_row kb_stage.kb_proposals%rowtype;
  v_principal kb_stage.kb_review_principals%rowtype;
  v_reviewed_at timestamptz;
begin
  if nullif(pg_catalog.btrim(p_expected_reviewed_by_handle), '') is null then
    raise exception 'approve_strict_proposal: expected reviewer handle is required';
  end if;
  if nullif(pg_catalog.btrim(p_review_note), '') is null then
    raise exception 'approve_strict_proposal: review note is required';
  end if;

  select * into v_principal
    from kb_stage.kb_review_principals
   where db_role = session_user::name
     and active;
  if not found then
    raise exception 'approve_strict_proposal: session role % has no active reviewer principal',
      session_user;
  end if;
  if v_principal.reviewed_by_handle <> pg_catalog.btrim(p_expected_reviewed_by_handle) then
    raise exception 'approve_strict_proposal: session role % is mapped to reviewer %, not %',
      session_user, v_principal.reviewed_by_handle, p_expected_reviewed_by_handle;
  end if;

  select * into v_row
    from kb_stage.kb_proposals
   where id = p_proposal_id
     and status = 'pending_review'
     and proposal_type = p_expected_proposal_type
     and payload = p_expected_payload
   for update;
  if not found then
    raise exception 'approve_strict_proposal: proposal % is not the exact pending snapshot',
      p_proposal_id;
  end if;
  if v_row.proposal_type not in ('revise_strategy', 'add_edge', 'attach_evidence', 'approve_claim') then
    raise exception 'approve_strict_proposal: unsupported proposal_type %', v_row.proposal_type;
  end if;
  if pg_catalog.jsonb_typeof(v_row.payload) <> 'object'
     or not (v_row.payload ? 'apply_payload')
     or pg_catalog.jsonb_typeof(v_row.payload->'apply_payload') <> 'object' then
    raise exception 'approve_strict_proposal: proposal % lacks object payload.apply_payload',
      p_proposal_id;
  end if;

  v_reviewed_at := pg_catalog.clock_timestamp();
  update kb_stage.kb_proposals
     set status = 'approved',
         reviewed_by_handle = v_principal.reviewed_by_handle,
         reviewed_by_agent_id = v_principal.reviewed_by_agent_id,
         reviewed_at = v_reviewed_at,
         review_note = pg_catalog.btrim(p_review_note),
         updated_at = pg_catalog.clock_timestamp()
   where id = p_proposal_id
     and status = 'pending_review'
     and proposal_type = p_expected_proposal_type
     and payload = p_expected_payload
   returning * into v_row;
  if not found then
    raise exception 'approve_strict_proposal: proposal % changed while approval was recorded',
      p_proposal_id;
  end if;

  insert into kb_stage.kb_proposal_approvals
    (proposal_id, proposal_type, payload, reviewed_by_handle,
     reviewed_by_agent_id, reviewed_by_db_role, reviewed_at, review_note,
     approved_proposal_snapshot)
  values
    (v_row.id, v_row.proposal_type, v_row.payload, v_principal.reviewed_by_handle,
     v_principal.reviewed_by_agent_id, session_user::name, v_reviewed_at,
     pg_catalog.btrim(p_review_note), pg_catalog.to_jsonb(v_row));

  return pg_catalog.jsonb_build_object(
    'id', v_row.id::text,
    'proposal_type', v_row.proposal_type,
    'status', v_row.status,
    'reviewed_by_handle', v_row.reviewed_by_handle,
    'reviewed_by_agent_id', v_row.reviewed_by_agent_id::text,
    'reviewed_by_db_role', session_user::text,
    'reviewed_at', v_row.reviewed_at::text,
    'review_note', v_row.review_note
  );
end
$function$;

-- Apply-side precondition. It locks the proposal for the surrounding transaction
-- and compares both the mutable ledger and immutable approval snapshot.
create or replace function kb_stage.assert_approved_proposal(
  p_proposal_id uuid,
  p_proposal_type text,
  p_payload jsonb,
  p_reviewed_by_handle text,
  p_reviewed_by_agent_id uuid,
  p_reviewed_at timestamptz,
  p_review_note text
) returns void
language plpgsql
security definer
set search_path = pg_catalog, pg_temp
set timezone = 'UTC'
as $function$
begin
  perform 1
    from kb_stage.kb_proposals p
    join kb_stage.kb_proposal_approvals a on a.proposal_id = p.id
   where p.id = p_proposal_id
     and p.status = 'approved'
     and p.proposal_type = p_proposal_type
     and p.payload = p_payload
     and p.reviewed_by_handle = p_reviewed_by_handle
     and p.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
     and p.reviewed_at = p_reviewed_at
     and p.review_note = p_review_note
     and a.proposal_type = p_proposal_type
     and a.payload = p_payload
     and a.reviewed_by_handle = p_reviewed_by_handle
     and a.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
     and a.reviewed_at = p_reviewed_at
     and a.review_note = p_review_note
     and a.approved_proposal_snapshot = pg_catalog.to_jsonb(p)
   for update of p;
  if not found then
    raise exception 'assert_approved_proposal: proposal % is not the exact immutable approved snapshot',
      p_proposal_id;
  end if;
end
$function$;

-- Apply-side terminal transition. It repeats every snapshot comparison while
-- the proposal row remains locked, resolves a real service-agent FK, and changes
-- ledger fields only. kb_apply remains a trusted narrow canonical writer; the
-- Python transaction performs and independently verifies typed writes first.
create or replace function kb_stage.finish_approved_proposal(
  p_proposal_id uuid,
  p_proposal_type text,
  p_payload jsonb,
  p_reviewed_by_handle text,
  p_reviewed_by_agent_id uuid,
  p_reviewed_at timestamptz,
  p_review_note text,
  p_applied_by_handle text
) returns jsonb
language plpgsql
security definer
set search_path = pg_catalog, pg_temp
set timezone = 'UTC'
as $function$
declare
  v_row kb_stage.kb_proposals%rowtype;
  v_applied_by_agent_id uuid;
begin
  if nullif(pg_catalog.btrim(p_applied_by_handle), '') is null then
    raise exception 'finish_approved_proposal: applied-by handle is required';
  end if;

  perform 1
    from kb_stage.kb_proposals p
    join kb_stage.kb_proposal_approvals a on a.proposal_id = p.id
   where p.id = p_proposal_id
     and p.status = 'approved'
     and p.proposal_type = p_proposal_type
     and p.payload = p_payload
     and p.reviewed_by_handle = p_reviewed_by_handle
     and p.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
     and p.reviewed_at = p_reviewed_at
     and p.review_note = p_review_note
     and a.proposal_type = p_proposal_type
     and a.payload = p_payload
     and a.reviewed_by_handle = p_reviewed_by_handle
     and a.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
     and a.reviewed_at = p_reviewed_at
     and a.review_note = p_review_note
     and a.approved_proposal_snapshot = pg_catalog.to_jsonb(p)
   for update of p;
  if not found then
    raise exception 'finish_approved_proposal: proposal % is not the locked immutable approved snapshot',
      p_proposal_id;
  end if;

  select id into v_applied_by_agent_id
    from public.agents
   where handle = pg_catalog.btrim(p_applied_by_handle)
   limit 1;
  if v_applied_by_agent_id is null then
    raise exception 'finish_approved_proposal: applied-by handle % has no public.agents row',
      p_applied_by_handle;
  end if;

  update kb_stage.kb_proposals
     set status = 'applied',
         applied_by_handle = pg_catalog.btrim(p_applied_by_handle),
         applied_by_agent_id = v_applied_by_agent_id,
         applied_at = pg_catalog.clock_timestamp(),
         updated_at = pg_catalog.clock_timestamp()
   where id = p_proposal_id
     and status = 'approved'
     and proposal_type = p_proposal_type
     and payload = p_payload
     and reviewed_by_handle = p_reviewed_by_handle
     and reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
     and reviewed_at = p_reviewed_at
     and review_note = p_review_note
   returning * into v_row;
  if not found then
    raise exception 'finish_approved_proposal: proposal % changed during apply', p_proposal_id;
  end if;

  return pg_catalog.jsonb_build_object(
    'id', v_row.id::text,
    'proposal_type', v_row.proposal_type,
    'status', v_row.status,
    'applied_by_handle', v_row.applied_by_handle,
    'applied_by_agent_id', v_row.applied_by_agent_id::text,
    'applied_at', v_row.applied_at::text
  );
end
$function$;

-- Read-only transition export. kb_apply receives only EXECUTE on this function,
-- never SELECT on the approval table. The stored approved row must be a complete
-- kb_proposals record and differ from the applied row only by the guarded apply
-- transition before any private material leaves PostgreSQL.
create or replace function kb_stage.export_applied_proposal_replay_rows(
  p_proposal_id uuid
) returns jsonb
language plpgsql
stable
security definer
set search_path = pg_catalog, pg_temp
set timezone = 'UTC'
as $function$
declare
  v_rows jsonb;
begin
  select pg_catalog.jsonb_build_object(
           'approved_proposal', approval.approved_proposal_snapshot,
           'approval_snapshot',
             pg_catalog.to_jsonb(approval) - 'approved_proposal_snapshot',
           'applied_proposal', pg_catalog.to_jsonb(proposal)
         )
    into v_rows
    from kb_stage.kb_proposals proposal
    join kb_stage.kb_proposal_approvals approval
      on approval.proposal_id = proposal.id
   where proposal.id = p_proposal_id
     and proposal.status = 'applied'
     and proposal.applied_by_handle is not null
     and proposal.applied_by_agent_id is not null
     and proposal.applied_at is not null
     and proposal.reviewed_by_handle is not null
     and proposal.reviewed_by_agent_id is not null
     and proposal.reviewed_at is not null
     and proposal.review_note is not null
     and nullif(pg_catalog.btrim(approval.reviewed_by_db_role::text), '') is not null
     and pg_catalog.jsonb_typeof(approval.approved_proposal_snapshot) = 'object'
     and approval.approved_proposal_snapshot = pg_catalog.to_jsonb(
           pg_catalog.jsonb_populate_record(
             null::kb_stage.kb_proposals,
             approval.approved_proposal_snapshot
           )
         )
     and approval.approved_proposal_snapshot->>'status' = 'approved'
     and approval.approved_proposal_snapshot->'applied_by_handle' = 'null'::jsonb
     and approval.approved_proposal_snapshot->'applied_by_agent_id' = 'null'::jsonb
     and approval.approved_proposal_snapshot->'applied_at' = 'null'::jsonb
     and approval.proposal_id = (approval.approved_proposal_snapshot->>'id')::uuid
     and approval.proposal_type = approval.approved_proposal_snapshot->>'proposal_type'
     and approval.payload = approval.approved_proposal_snapshot->'payload'
     and approval.reviewed_by_handle =
           approval.approved_proposal_snapshot->>'reviewed_by_handle'
     and approval.reviewed_by_agent_id =
           (approval.approved_proposal_snapshot->>'reviewed_by_agent_id')::uuid
     and approval.reviewed_at =
           (approval.approved_proposal_snapshot->>'reviewed_at')::timestamptz
     and approval.review_note = approval.approved_proposal_snapshot->>'review_note'
     and (
       approval.approved_proposal_snapshot || pg_catalog.jsonb_build_object(
         'status', proposal.status,
         'applied_by_handle', proposal.applied_by_handle,
         'applied_by_agent_id', proposal.applied_by_agent_id,
         'applied_at', proposal.applied_at,
         'updated_at', proposal.updated_at
       )
     ) = pg_catalog.to_jsonb(proposal);

  if v_rows is null then
    raise exception 'export_applied_proposal_replay_rows: exact applied transition unavailable for proposal %',
      p_proposal_id;
  end if;
  return v_rows;
end
$function$;

alter function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
  owner to kb_gate_owner;
alter function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text)
  owner to kb_gate_owner;
alter function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text)
  owner to kb_gate_owner;
alter function kb_stage.export_applied_proposal_replay_rows(uuid)
  owner to kb_gate_owner;

revoke all on function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
  from public, kb_gate_owner, kb_apply, kb_review;
revoke all on function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text)
  from public, kb_gate_owner, kb_apply, kb_review;
revoke all on function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text)
  from public, kb_gate_owner, kb_apply, kb_review;
revoke all on function kb_stage.export_applied_proposal_replay_rows(uuid)
  from public, kb_gate_owner, kb_apply, kb_review;

grant execute on function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
  to kb_review;
grant execute on function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text)
  to kb_apply;
grant execute on function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text)
  to kb_apply;
grant execute on function kb_stage.export_applied_proposal_replay_rows(uuid)
  to kb_apply;

-- Assert the complete protected catalog contract before commit. These checks
-- make reruns an upgrade proof rather than a best-effort sequence of grants.
do $final_invariants$
declare
  v_drift text;
begin
  select pg_catalog.string_agg(role.rolname, ', ' order by role.rolname)
    into v_drift
    from pg_catalog.pg_roles role
   where role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
     and (
       role.rolsuper
       or role.rolinherit
       or role.rolcreatedb
       or role.rolcreaterole
       or role.rolreplication
       or role.rolbypassrls
       or (role.rolname = 'kb_gate_owner' and role.rolcanlogin)
       or (role.rolname in ('kb_review', 'kb_apply') and not role.rolcanlogin)
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected role attributes did not normalize: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format('%I -> %I', member_role.rolname, granted_role.rolname),
           ', ' order by member_role.rolname, granted_role.rolname
         )
    into v_drift
    from pg_catalog.pg_auth_members membership
    join pg_catalog.pg_roles member_role on member_role.oid = membership.member
    join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
   where member_role.rolname in (
           'kb_gate_owner', 'kb_review', 'kb_apply', 'kb_observatory_read',
           'leoclean_kb_runtime', 'leoclean_kb_stage_owner'
         )
      or granted_role.rolname in (
           'kb_gate_owner', 'kb_review', 'kb_apply',
           'leoclean_kb_runtime', 'leoclean_kb_stage_owner'
         );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected role membership appeared during migration: %', v_drift;
  end if;

  if exists (select 1 from public.agents where handle = 'm3ta')
     or not exists (
       select 1
         from kb_stage.kb_review_principals principal
         join public.agents reviewer
           on reviewer.id = principal.reviewed_by_agent_id
        where principal.db_role = 'kb_review'::name
          and principal.reviewed_by_handle = 'm3taversal'
          and principal.active
          and reviewer.handle = 'm3taversal'
     ) then
    raise exception 'kb_apply_prereqs: kb_review principal did not normalize to active m3taversal';
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I on %I usage=%s create=%s',
             protected_role.rolname,
             protected_schema.nspname,
             pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'USAGE'),
             pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'CREATE')
           ),
           ', ' order by protected_role.rolname, protected_schema.nspname
         )
    into v_drift
    from (values ('kb_gate_owner'), ('kb_review'), ('kb_apply')) protected_role(rolname)
    cross join (values ('public'), ('kb_stage')) protected_schema(nspname)
   where not pg_catalog.has_schema_privilege(
               protected_role.rolname, protected_schema.nspname, 'USAGE'
             )
      or pg_catalog.has_schema_privilege(
           protected_role.rolname, protected_schema.nspname, 'CREATE'
         );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected schema ACL mismatch: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format('%I.%I owned by %I', namespace.nspname, relation.relname, owner_role.rolname),
           ', ' order by namespace.nspname, relation.relname
         )
    into v_drift
    from pg_catalog.pg_class relation
    join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
   where (namespace.nspname, relation.relname) in (
           ('public', 'agents'),
           ('public', 'strategies'),
           ('public', 'strategy_nodes'),
           ('public', 'claim_evidence'),
           ('public', 'claim_evidence_assessments'),
           ('public', 'claim_edges'),
           ('public', 'claims'),
           ('public', 'sources'),
           ('public', 'reasoning_tools'),
           ('kb_stage', 'kb_proposals'),
           ('kb_stage', 'kb_review_principals'),
           ('kb_stage', 'kb_proposal_approvals'),
           ('kb_stage', 'teleo_v3_installation_ownership')
         )
     and not (
       (namespace.nspname = 'kb_stage'
        and relation.relname in (
          'kb_review_principals', 'kb_proposal_approvals', 'teleo_v3_installation_ownership'
        )
        and owner_role.rolname = 'kb_gate_owner')
       or (not (
             namespace.nspname = 'kb_stage'
             and relation.relname in (
               'kb_review_principals', 'kb_proposal_approvals', 'teleo_v3_installation_ownership'
             )
           )
           and owner_role.rolname = current_user)
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected table ownership mismatch: %', v_drift;
  end if;

  if not exists (
    select 1
      from pg_catalog.pg_attribute attribute
     where attribute.attrelid = 'kb_stage.kb_proposal_approvals'::regclass
       and attribute.attname = 'approved_proposal_snapshot'
       and attribute.atttypid = 'pg_catalog.jsonb'::regtype
       and not attribute.attnotnull
       and not attribute.attisdropped
  ) then
    raise exception 'kb_apply_prereqs: approved proposal snapshot column is not nullable JSONB';
  end if;

  with expected(signature) as (
    values
      ('approve_strict_proposal(uuid, text, jsonb, text, text)'),
      ('assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)'),
      ('finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)'),
      ('export_applied_proposal_replay_rows(uuid)')
  ), actual(signature) as (
    select procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')'
      from pg_catalog.pg_proc procedure
      join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
     where namespace.nspname = 'kb_stage'
       and procedure.proname in (
             'approve_strict_proposal',
             'assert_approved_proposal',
             'finish_approved_proposal',
             'export_applied_proposal_replay_rows'
           )
  ), delta(kind, signature) as (
    select 'unexpected', actual.signature
      from actual
     where not exists (select 1 from expected where expected.signature = actual.signature)
    union all
    select 'missing', expected.signature
      from expected
     where not exists (select 1 from actual where actual.signature = expected.signature)
  )
  select pg_catalog.string_agg(kind || ' ' || signature, ', ' order by kind, signature)
    into v_drift
    from delta;

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected function signature mismatch: %', v_drift;
  end if;

  select pg_catalog.string_agg(
           pg_catalog.format(
             '%I.%I(%s) owner=%I security_definer=%s config=%s',
             namespace.nspname,
             procedure.proname,
             pg_catalog.oidvectortypes(procedure.proargtypes),
             owner_role.rolname,
             procedure.prosecdef,
             procedure.proconfig
           ),
           ', ' order by procedure.proname
         )
    into v_drift
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
   where namespace.nspname = 'kb_stage'
     and procedure.proname in (
           'approve_strict_proposal',
           'assert_approved_proposal',
           'finish_approved_proposal',
           'export_applied_proposal_replay_rows'
         )
     and (
       owner_role.rolname <> 'kb_gate_owner'
       or not procedure.prosecdef
       or pg_catalog.cardinality(procedure.proconfig) <> 2
       or not procedure.proconfig @> array[
         'search_path=pg_catalog, pg_temp'::text,
         'TimeZone=UTC'::text
       ]
       or (
         procedure.proname = 'export_applied_proposal_replay_rows'
         and (
           procedure.provolatile <> 's'
           or procedure.prokind <> 'f'
           or procedure.prorettype <> 'pg_catalog.jsonb'::regtype
         )
       )
     );

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected function attribute mismatch: %', v_drift;
  end if;

  with tracked(schema_name, object_name) as (
    values
      ('public', 'agents'),
      ('public', 'strategies'),
      ('public', 'strategy_nodes'),
      ('public', 'claim_evidence'),
      ('public', 'claim_evidence_assessments'),
      ('public', 'claim_edges'),
      ('public', 'claims'),
      ('public', 'sources'),
      ('public', 'reasoning_tools'),
      ('kb_stage', 'kb_proposals'),
      ('kb_stage', 'kb_review_principals'),
      ('kb_stage', 'kb_proposal_approvals'),
      ('kb_stage', 'teleo_v3_installation_ownership')
  ), actual(schema_name, object_name, column_name, grantee, privilege_type, is_grantable) as (
    select namespace.nspname::text,
           relation.relname::text,
           attribute.attname::text,
           (case
              when acl.grantee = 0 then 'PUBLIC'
              else pg_catalog.pg_get_userbyid(acl.grantee)
            end)::text,
           pg_catalog.upper(acl.privilege_type),
           acl.is_grantable
      from tracked
      join pg_catalog.pg_namespace namespace on namespace.nspname = tracked.schema_name
      join pg_catalog.pg_class relation
        on relation.relnamespace = namespace.oid
       and relation.relname = tracked.object_name
      join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
     cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
     where attribute.attnum > 0
       and not attribute.attisdropped
       and acl.grantee <> relation.relowner
  ), expected(schema_name, object_name, column_name, grantee, privilege_type, is_grantable) as (
    select preserved.schema_name::text,
           preserved.relation_name::text,
           preserved.column_name::text,
           preserved.grantee::text,
           preserved.privilege_type,
           preserved.is_grantable
      from pg_temp.kb_apply_prereqs_preserved_column_acls preserved
  ), delta(kind, schema_name, object_name, column_name, grantee, privilege_type, is_grantable) as (
    select 'unexpected', actual.*
      from actual
     where not exists (
       select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual)
     )
    union all
    select 'missing', expected.*
      from expected
     where not exists (
       select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected)
     )
  )
  select pg_catalog.string_agg(
           pg_catalog.format(
             '%s %I.%I.%I -> %s %s grantable=%s',
             kind,
             schema_name,
             object_name,
             column_name,
             grantee,
             privilege_type,
             is_grantable
           ),
           ', ' order by kind, schema_name, object_name, column_name, grantee, privilege_type
         )
    into v_drift
    from delta;

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected column ACL mismatch: %', v_drift;
  end if;

  with core_expected(schema_name, object_name, grantee, privilege_type, is_grantable) as (
    values
      ('public', 'agents', 'kb_gate_owner', 'SELECT', false),
      ('public', 'agents', 'kb_apply', 'SELECT', false),
      ('public', 'agents', 'kb_review', 'SELECT', false),
      ('public', 'strategies', 'kb_apply', 'SELECT', false),
      ('public', 'strategies', 'kb_apply', 'INSERT', false),
      ('public', 'strategies', 'kb_apply', 'UPDATE', false),
      ('public', 'strategy_nodes', 'kb_apply', 'SELECT', false),
      ('public', 'strategy_nodes', 'kb_apply', 'INSERT', false),
      ('public', 'strategy_nodes', 'kb_apply', 'UPDATE', false),
      ('public', 'claim_evidence', 'kb_apply', 'SELECT', false),
      ('public', 'claim_evidence', 'kb_apply', 'INSERT', false),
      ('public', 'claim_edges', 'kb_apply', 'SELECT', false),
      ('public', 'claim_edges', 'kb_apply', 'INSERT', false),
      ('public', 'claims', 'kb_apply', 'SELECT', false),
      ('public', 'claims', 'kb_apply', 'INSERT', false),
      ('public', 'sources', 'kb_apply', 'SELECT', false),
      ('public', 'sources', 'kb_apply', 'INSERT', false),
      ('public', 'reasoning_tools', 'kb_apply', 'SELECT', false),
      ('public', 'reasoning_tools', 'kb_apply', 'INSERT', false),
      ('kb_stage', 'kb_proposals', 'kb_gate_owner', 'SELECT', false),
      ('kb_stage', 'kb_proposals', 'kb_gate_owner', 'UPDATE', false),
      ('kb_stage', 'kb_proposals', 'kb_apply', 'SELECT', false),
      ('kb_stage', 'kb_proposals', 'kb_review', 'SELECT', false)
    union all
    select 'public', 'claim_evidence_assessments', 'kb_apply', 'SELECT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
    union all
    select 'public', 'claim_evidence_assessments', 'kb_apply', 'INSERT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
    union all
    select 'public', 'claims', 'kb_gate_owner', 'SELECT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
    union all
    select 'public', 'sources', 'kb_gate_owner', 'SELECT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
    union all
    select 'public', 'claim_evidence', 'kb_gate_owner', 'SELECT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
    union all
    select 'public', 'claim_edges', 'kb_gate_owner', 'SELECT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
    union all
    select 'public', 'claim_evidence_assessments', 'kb_gate_owner', 'SELECT', false
     where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
  ), expected(schema_name, object_name, grantee, privilege_type, is_grantable) as (
    select * from core_expected
    union all
    select preserved.schema_name::text,
           preserved.relation_name::text,
           preserved.grantee::text,
           preserved.privilege_type,
           preserved.is_grantable
      from pg_temp.kb_apply_prereqs_preserved_table_acls preserved
  ), tracked(schema_name, object_name) as (
    values
      ('public', 'agents'),
      ('public', 'strategies'),
      ('public', 'strategy_nodes'),
      ('public', 'claim_evidence'),
      ('public', 'claim_evidence_assessments'),
      ('public', 'claim_edges'),
      ('public', 'claims'),
      ('public', 'sources'),
      ('public', 'reasoning_tools'),
      ('kb_stage', 'kb_proposals'),
      ('kb_stage', 'kb_review_principals'),
      ('kb_stage', 'kb_proposal_approvals'),
      ('kb_stage', 'teleo_v3_installation_ownership')
  ), actual(schema_name, object_name, grantee, privilege_type, is_grantable) as (
    select distinct
           namespace.nspname::text,
           relation.relname::text,
           (case
              when acl.grantee = 0 then 'PUBLIC'
              else grantee_role.rolname
            end)::text,
           pg_catalog.upper(acl.privilege_type),
           acl.is_grantable
      from tracked
      join pg_catalog.pg_namespace namespace on namespace.nspname = tracked.schema_name
      join pg_catalog.pg_class relation
        on relation.relnamespace = namespace.oid
       and relation.relname = tracked.object_name
      cross join lateral pg_catalog.aclexplode(
        coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
      ) acl
      left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
     where acl.grantee <> relation.relowner
  ), delta(kind, schema_name, object_name, grantee, privilege_type, is_grantable) as (
    select 'unexpected', actual.*
     from actual
     where not exists (
       select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual)
     )
    union all
    select 'missing', expected.*
     from expected
     where not exists (
       select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected)
     )
  )
  select pg_catalog.string_agg(
           pg_catalog.format(
             '%s %I.%I -> %I %s grantable=%s',
             kind, schema_name, object_name, grantee, privilege_type, is_grantable
           ),
           ', ' order by kind, schema_name, object_name, grantee, privilege_type
         )
    into v_drift
    from delta;

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected table ACL mismatch: %', v_drift;
  end if;

  with expected(signature, grantee, privilege_type, is_grantable) as (
    values
      ('approve_strict_proposal(uuid, text, jsonb, text, text)', 'kb_review', 'EXECUTE', false),
      ('assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)',
       'kb_apply', 'EXECUTE', false),
      ('finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)',
       'kb_apply', 'EXECUTE', false),
      ('export_applied_proposal_replay_rows(uuid)',
       'kb_apply', 'EXECUTE', false)
  ), actual(signature, grantee, privilege_type, is_grantable) as (
    select distinct
           procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')',
           (case
              when acl.grantee = 0 then 'PUBLIC'
              else pg_catalog.pg_get_userbyid(acl.grantee)
            end)::text,
           pg_catalog.upper(acl.privilege_type),
           acl.is_grantable
      from pg_catalog.pg_proc procedure
      join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
      cross join lateral pg_catalog.aclexplode(
        coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
      ) acl
     where namespace.nspname = 'kb_stage'
       and procedure.proname in (
             'approve_strict_proposal',
             'assert_approved_proposal',
             'finish_approved_proposal',
             'export_applied_proposal_replay_rows'
           )
       and acl.grantee <> procedure.proowner
  ), delta(kind, signature, grantee, privilege_type, is_grantable) as (
    select 'unexpected', actual.*
     from actual
     where not exists (
       select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual)
     )
    union all
    select 'missing', expected.*
     from expected
     where not exists (
       select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected)
     )
  )
  select pg_catalog.string_agg(
           pg_catalog.format(
             '%s %s -> %I %s grantable=%s',
             kind, signature, grantee, privilege_type, is_grantable
           ),
           ', ' order by kind, signature, grantee, privilege_type
         )
    into v_drift
    from delta;

  if v_drift is not null then
    raise exception 'kb_apply_prereqs: protected function ACL mismatch: %', v_drift;
  end if;
end
$final_invariants$;

commit;
