


SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;


CREATE SCHEMA IF NOT EXISTS "admin";


ALTER SCHEMA "admin" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "agents";


ALTER SCHEMA "agents" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "ai";


ALTER SCHEMA "ai" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "analytics";


ALTER SCHEMA "analytics" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "audit";


ALTER SCHEMA "audit" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "authz";


ALTER SCHEMA "authz" OWNER TO "postgres";


COMMENT ON SCHEMA "authz" IS 'Private schema for device approval requests and time-bounded developer tokens. Not exposed via PostgREST; clients use public RPC wrappers only.';



CREATE SCHEMA IF NOT EXISTS "automation";


ALTER SCHEMA "automation" OWNER TO "postgres";


COMMENT ON SCHEMA "automation" IS 'Phase U: event bus + trigger-rule dispatcher. Producer triggers across the platform emit rows into automation.events; automation.trigger_rules + the automation-dispatcher cron drain those events into actions (workflow, webhook via audit.webhook_outbox, notification via public.fn_insert_notification).';



CREATE SCHEMA IF NOT EXISTS "battles";


ALTER SCHEMA "battles" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "billing";


ALTER SCHEMA "billing" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "connectors";


ALTER SCHEMA "connectors" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "content";


ALTER SCHEMA "content" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "core";


ALTER SCHEMA "core" OWNER TO "postgres";


CREATE EXTENSION IF NOT EXISTS "pg_cron" WITH SCHEMA "pg_catalog";






CREATE SCHEMA IF NOT EXISTS "devices";


ALTER SCHEMA "devices" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "execution";


ALTER SCHEMA "execution" OWNER TO "postgres";


COMMENT ON SCHEMA "execution" IS 'D12: poll/complete RPCs are service_role only (called by the poll-async-executions Edge Function which uses the service-role key).';



CREATE SCHEMA IF NOT EXISTS "i18n";


ALTER SCHEMA "i18n" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "identity_gov";


ALTER SCHEMA "identity_gov" OWNER TO "postgres";


COMMENT ON SCHEMA "identity_gov" IS 'Namespace governance authority. Single source of truth for reserved handles. Write access only through SECURITY DEFINER RPCs.';



CREATE SCHEMA IF NOT EXISTS "integrations";


ALTER SCHEMA "integrations" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "internal";


ALTER SCHEMA "internal" OWNER TO "postgres";


COMMENT ON SCHEMA "internal" IS 'Private helper functions. Not exposed via PostgREST.';



CREATE SCHEMA IF NOT EXISTS "lensers";


ALTER SCHEMA "lensers" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "lenses";


ALTER SCHEMA "lenses" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "media";


ALTER SCHEMA "media" OWNER TO "postgres";


COMMENT ON SCHEMA "media" IS 'Normalized media/file storage layer. Replaces ai.resources as the canonical file registry. Workspace-scoped with explicit lifecycle, visibility, and attachment bindings.';



CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";






CREATE SCHEMA IF NOT EXISTS "organizations";


ALTER SCHEMA "organizations" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "platform";


ALTER SCHEMA "platform" OWNER TO "postgres";


COMMENT ON SCHEMA "public" IS 'standard public schema';



CREATE SCHEMA IF NOT EXISTS "reputation";


ALTER SCHEMA "reputation" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "status";


ALTER SCHEMA "status" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "tenancy";


ALTER SCHEMA "tenancy" OWNER TO "postgres";


COMMENT ON SCHEMA "tenancy" IS 'Workspace tenancy: workspaces, members, roles. Every tenant-owned resource references a workspace_id.';



CREATE SCHEMA IF NOT EXISTS "wallet";


ALTER SCHEMA "wallet" OWNER TO "postgres";


CREATE SCHEMA IF NOT EXISTS "xp";


ALTER SCHEMA "xp" OWNER TO "postgres";


CREATE EXTENSION IF NOT EXISTS "btree_gist" WITH SCHEMA "extensions";






CREATE EXTENSION IF NOT EXISTS "fuzzystrmatch" WITH SCHEMA "extensions";






CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";






CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";






CREATE EXTENSION IF NOT EXISTS "pg_trgm" WITH SCHEMA "extensions";






CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";






CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";






CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";






CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";






CREATE TYPE "ai"."ai_capability_enum" AS ENUM (
    'text',
    'image',
    'code',
    'music'
);


ALTER TYPE "ai"."ai_capability_enum" OWNER TO "postgres";


CREATE TYPE "ai"."key_scope_enum" AS ENUM (
    'agent',
    'user',
    'team'
);


ALTER TYPE "ai"."key_scope_enum" OWNER TO "postgres";


COMMENT ON TYPE "ai"."key_scope_enum" IS 'Scope of an ai.keys BYOK API key: agent (scoped to an AI actor), user (scoped to a lenser), team (scoped to a team/org).';



CREATE TYPE "ai"."key_status_enum" AS ENUM (
    'active',
    'revoked',
    'expired'
);


ALTER TYPE "ai"."key_status_enum" OWNER TO "postgres";


COMMENT ON TYPE "ai"."key_status_enum" IS 'Lifecycle status of an ai.keys BYOK API key.';



CREATE TYPE "ai"."media_type" AS ENUM (
    'text',
    'image',
    'audio',
    'video',
    'document',
    'json',
    'binary'
);


ALTER TYPE "ai"."media_type" OWNER TO "postgres";


CREATE TYPE "ai"."model_tier_enum" AS ENUM (
    'free',
    'paid',
    'enterprise'
);


ALTER TYPE "ai"."model_tier_enum" OWNER TO "postgres";


COMMENT ON TYPE "ai"."model_tier_enum" IS 'Access tier for AI models (free, paid, enterprise). Relocates public.pricing_tier_enum to the ai schema where it conceptually belongs. Use ai.model_tier_enum for all new code.';



CREATE TYPE "ai"."provider_enum" AS ENUM (
    'openai',
    'anthropic',
    'google',
    'custom',
    'xai',
    'meta',
    'mistral',
    'local'
);


ALTER TYPE "ai"."provider_enum" OWNER TO "postgres";


CREATE TYPE "ai"."resource_type" AS ENUM (
    'attachment',
    'dataset',
    'example',
    'reference'
);


ALTER TYPE "ai"."resource_type" OWNER TO "postgres";


CREATE TYPE "ai"."unit_type_enum" AS ENUM (
    'tokens',
    'image',
    'video_second',
    'audio_second'
);


ALTER TYPE "ai"."unit_type_enum" OWNER TO "postgres";


COMMENT ON TYPE "ai"."unit_type_enum" IS 'Billing unit type for AI model pricing. Replaces text CHECK constraint ''model_pricing_unit_type_check'' on ai.model_pricing.unit_type.';



CREATE TYPE "analytics"."feedback_status_enum" AS ENUM (
    'pending',
    'in_progress',
    'resolved',
    'closed'
);


ALTER TYPE "analytics"."feedback_status_enum" OWNER TO "postgres";


CREATE TYPE "analytics"."product_tag_enum" AS ENUM (
    'bug',
    'feature',
    'ui_ux',
    'general',
    'other'
);


ALTER TYPE "analytics"."product_tag_enum" OWNER TO "postgres";


CREATE TYPE "audit"."security_event_type_enum" AS ENUM (
    'byok_key_registered',
    'byok_key_revoked',
    'byok_key_rotated',
    'byok_key_used',
    'byok_key_expired',
    'local_agent_synced',
    'local_agent_hmac_verified',
    'local_agent_hmac_failed',
    'suspicious_activity',
    'rate_limit_exceeded',
    'unauthorized_access_attempt'
);


ALTER TYPE "audit"."security_event_type_enum" OWNER TO "postgres";


COMMENT ON TYPE "audit"."security_event_type_enum" IS 'Known security event types. Replaces text CHECK constraint ''security_events_type_check'' on audit.security_events.event_type. Adding new event types requires: ALTER TYPE audit.security_event_type_enum ADD VALUE ''new_type''; This is a non-blocking DDL operation in Postgres.';



CREATE TYPE "authz"."developer_token_status_enum" AS ENUM (
    'active',
    'revoked',
    'expired'
);


ALTER TYPE "authz"."developer_token_status_enum" OWNER TO "postgres";


CREATE TYPE "authz"."device_approval_request_status_enum" AS ENUM (
    'pending',
    'approved',
    'exchanged',
    'expired'
);


ALTER TYPE "authz"."device_approval_request_status_enum" OWNER TO "postgres";


CREATE TYPE "battles"."battle_status_enum" AS ENUM (
    'draft',
    'open',
    'executing',
    'voting',
    'scoring',
    'closed',
    'published',
    'archived'
);


ALTER TYPE "battles"."battle_status_enum" OWNER TO "postgres";


CREATE TYPE "battles"."battle_type_enum" AS ENUM (
    'ai_vs_ai',
    'human_vs_human_ai_votes',
    'human_vs_human_open_votes',
    'human_vs_ai',
    'workflow_battle',
    'lenser_battle'
);


ALTER TYPE "battles"."battle_type_enum" OWNER TO "postgres";


COMMENT ON TYPE "battles"."battle_type_enum" IS 'Battle competition formats. workflow_battle: both contenders run a Connected Lens workflow pipeline; leaf node outputs are submitted as the battle entry.';



CREATE TYPE "battles"."contender_structure_enum" AS ENUM (
    'ai_vs_ai',
    'human_vs_human',
    'human_vs_ai'
);


ALTER TYPE "battles"."contender_structure_enum" OWNER TO "postgres";


COMMENT ON TYPE "battles"."contender_structure_enum" IS 'Who competes in a battle. Orthogonal to judging_mode.';



CREATE TYPE "battles"."contender_type_enum" AS ENUM (
    'human',
    'ai_model',
    'ai_agent'
);


ALTER TYPE "battles"."contender_type_enum" OWNER TO "postgres";


CREATE TYPE "battles"."invite_link_type" AS ENUM (
    'public',
    'private',
    'link',
    'qr'
);


ALTER TYPE "battles"."invite_link_type" OWNER TO "postgres";


CREATE TYPE "battles"."judging_mode_enum" AS ENUM (
    'community_vote',
    'ai_judge',
    'rubric_score',
    'auto_score'
);


ALTER TYPE "battles"."judging_mode_enum" OWNER TO "postgres";


COMMENT ON TYPE "battles"."judging_mode_enum" IS 'How the winner of a battle is decided. Orthogonal to contender_structure.';



CREATE TYPE "battles"."scorecard_result_enum" AS ENUM (
    'pass',
    'fail',
    'partial',
    'skipped'
);


ALTER TYPE "battles"."scorecard_result_enum" OWNER TO "postgres";


CREATE TYPE "battles"."sponsorship_pool_status_enum" AS ENUM (
    'open',
    'locked',
    'distributing',
    'distributed',
    'refunded'
);


ALTER TYPE "battles"."sponsorship_pool_status_enum" OWNER TO "postgres";


CREATE TYPE "battles"."submission_status_enum" AS ENUM (
    'pending',
    'streaming',
    'submitted',
    'withdrawn',
    'disqualified'
);


ALTER TYPE "battles"."submission_status_enum" OWNER TO "postgres";


CREATE TYPE "battles"."vote_value_enum" AS ENUM (
    'contender_a',
    'contender_b',
    'draw'
);


ALTER TYPE "battles"."vote_value_enum" OWNER TO "postgres";


CREATE TYPE "battles"."voter_eligibility_enum" AS ENUM (
    'open',
    'human_only',
    'ai_only',
    'verified_lenser',
    'lenser_only'
);


ALTER TYPE "battles"."voter_eligibility_enum" OWNER TO "postgres";


COMMENT ON TYPE "battles"."voter_eligibility_enum" IS 'Who can cast votes in a battle. lenser_only: any lenser profile (not just verified). Syncs with the VotePanel frontend ELIGIBILITY_LABELS map.';



CREATE TYPE "billing"."order_status_enum" AS ENUM (
    'pending',
    'failed',
    'paid',
    'refunded',
    'partial_refund',
    'fraudulent'
);


ALTER TYPE "billing"."order_status_enum" OWNER TO "postgres";


CREATE TYPE "billing"."product_status_enum" AS ENUM (
    'draft',
    'published',
    'archived'
);


ALTER TYPE "billing"."product_status_enum" OWNER TO "postgres";


CREATE TYPE "content"."content_status" AS ENUM (
    'draft',
    'published',
    'archived'
);


ALTER TYPE "content"."content_status" OWNER TO "postgres";


CREATE TYPE "content"."entity_type_enum" AS ENUM (
    'thread',
    'lens',
    'battle',
    'thread_reply',
    'workflow',
    'agent'
);


ALTER TYPE "content"."entity_type_enum" OWNER TO "postgres";


CREATE TYPE "content"."payment_method_enum" AS ENUM (
    'byok',
    'wallet',
    'free'
);


ALTER TYPE "content"."payment_method_enum" OWNER TO "postgres";


CREATE TYPE "content"."reaction_enum" AS ENUM (
    'like',
    'dislike',
    'saved',
    'copy',
    'love',
    'clap'
);


ALTER TYPE "content"."reaction_enum" OWNER TO "postgres";


CREATE TYPE "content"."report_reason_enum" AS ENUM (
    'spam',
    'harassment',
    'misinformation',
    'off_topic',
    'other'
);


ALTER TYPE "content"."report_reason_enum" OWNER TO "postgres";


COMMENT ON TYPE "content"."report_reason_enum" IS 'Valid reasons for content reports. Replaces text CHECK constraint ''reports_reason_check'' on content.reports.reason.';



CREATE TYPE "content"."suggestion_status_enum" AS ENUM (
    'pending',
    'accepted',
    'rejected'
);


ALTER TYPE "content"."suggestion_status_enum" OWNER TO "postgres";


COMMENT ON TYPE "content"."suggestion_status_enum" IS 'Status of an AI-generated tag suggestion. Replaces text CHECK constraint ''tag_suggestions_status_check'' on content.tag_suggestions.status.';



CREATE TYPE "content"."tag_visibility_enum" AS ENUM (
    'public',
    'private',
    'hidden'
);


ALTER TYPE "content"."tag_visibility_enum" OWNER TO "postgres";


CREATE TYPE "content"."thread_reply_status" AS ENUM (
    'published',
    'hidden',
    'deleted'
);


ALTER TYPE "content"."thread_reply_status" OWNER TO "postgres";


CREATE TYPE "content"."thread_visibility" AS ENUM (
    'public',
    'community',
    'private'
);


ALTER TYPE "content"."thread_visibility" OWNER TO "postgres";


CREATE TYPE "content"."visibility_enum" AS ENUM (
    'public',
    'community',
    'private'
);


ALTER TYPE "content"."visibility_enum" OWNER TO "postgres";


CREATE TYPE "lensers"."group_member_role_enum" AS ENUM (
    'admin',
    'moderator',
    'member',
    'judge'
);


ALTER TYPE "lensers"."group_member_role_enum" OWNER TO "postgres";


CREATE TYPE "lensers"."group_type_enum" AS ENUM (
    'community',
    'team'
);


ALTER TYPE "lensers"."group_type_enum" OWNER TO "postgres";


CREATE TYPE "lensers"."group_visibility_enum" AS ENUM (
    'public',
    'private',
    'invite_only'
);


ALTER TYPE "lensers"."group_visibility_enum" OWNER TO "postgres";


CREATE TYPE "lensers"."lenser_badge_category" AS ENUM (
    'prestige',
    'achievement'
);


ALTER TYPE "lensers"."lenser_badge_category" OWNER TO "postgres";


CREATE TYPE "lensers"."lenser_badge_type" AS ENUM (
    'system',
    'community',
    'challenge',
    'prestige_first_10',
    'prestige_first_100',
    'prestige_first_1000',
    'achievement_xp_level',
    'achievement_xp_milestone',
    'COUNTRY_TOP_1',
    'COUNTRY_TOP_10',
    'COUNTRY_TOP_100',
    'FOUNDING_10',
    'FOUNDING_100',
    'FOUNDING_1000'
);


ALTER TYPE "lensers"."lenser_badge_type" OWNER TO "postgres";


CREATE TYPE "lensers"."lenser_social_platform" AS ENUM (
    'Behance',
    'Dribbble',
    'GitHub',
    'Instagram',
    'LinkedIn',
    'Twitch',
    'Website',
    'X',
    'Twitter',
    'Youtube',
    'Facebook'
);


ALTER TYPE "lensers"."lenser_social_platform" OWNER TO "postgres";


CREATE TYPE "lensers"."lenser_status" AS ENUM (
    'active',
    'suspended',
    'deactivated',
    'pending_deletion',
    'deleted'
);


ALTER TYPE "lensers"."lenser_status" OWNER TO "postgres";


CREATE TYPE "lensers"."lenser_type" AS ENUM (
    'human',
    'ai'
);


ALTER TYPE "lensers"."lenser_type" OWNER TO "postgres";


CREATE TYPE "lensers"."lenser_visibility" AS ENUM (
    'public',
    'community',
    'private'
);


ALTER TYPE "lensers"."lenser_visibility" OWNER TO "postgres";


CREATE TYPE "lensers"."profile_access_level" AS ENUM (
    'FULL_PROFILE',
    'RESTRICTED_PROFILE',
    'OWNER_RECOVERY_PROFILE',
    'UNAVAILABLE_PROFILE'
);


ALTER TYPE "lensers"."profile_access_level" OWNER TO "postgres";


CREATE TYPE "lensers"."relationship_status" AS ENUM (
    'pending',
    'accepted',
    'rejected',
    'blocked',
    'removed'
);


ALTER TYPE "lensers"."relationship_status" OWNER TO "postgres";


CREATE TYPE "lensers"."wallet_mode_enum" AS ENUM (
    'CLOUD',
    'BYOK',
    'LOCAL'
);


ALTER TYPE "lensers"."wallet_mode_enum" OWNER TO "postgres";


COMMENT ON TYPE "lensers"."wallet_mode_enum" IS 'Wallet/billing mode for a lenser: CLOUD = platform credits, BYOK = bring-your-own-key, LOCAL = self-hosted inference.';



CREATE TYPE "lenses"."workflow_trigger_type" AS ENUM (
    'cron',
    'battle_event',
    'webhook',
    'manual'
);


ALTER TYPE "lenses"."workflow_trigger_type" OWNER TO "postgres";


CREATE TYPE "organizations"."address_type_enum" AS ENUM (
    'headquarters',
    'billing',
    'shipping',
    'branch'
);


ALTER TYPE "organizations"."address_type_enum" OWNER TO "postgres";


CREATE TYPE "organizations"."member_role_enum" AS ENUM (
    'owner',
    'admin',
    'manager',
    'billing',
    'member',
    'viewer'
);


ALTER TYPE "organizations"."member_role_enum" OWNER TO "postgres";


CREATE TYPE "organizations"."org_type_enum" AS ENUM (
    'commercial',
    'nonprofit',
    'educational'
);


ALTER TYPE "organizations"."org_type_enum" OWNER TO "postgres";


CREATE TYPE "organizations"."status_enum" AS ENUM (
    'active',
    'inactive',
    'suspended',
    'closed'
);


ALTER TYPE "organizations"."status_enum" OWNER TO "postgres";


CREATE TYPE "public"."page_view_target_enum" AS ENUM (
    'thread',
    'thread_reply',
    'lens',
    'profile',
    'page',
    'battle'
);


ALTER TYPE "public"."page_view_target_enum" OWNER TO "postgres";


CREATE TYPE "status"."incident_impact" AS ENUM (
    'minor',
    'partial',
    'major'
);


ALTER TYPE "status"."incident_impact" OWNER TO "postgres";


CREATE TYPE "status"."incident_stage" AS ENUM (
    'investigating',
    'identified',
    'monitoring',
    'resolved',
    'postmortem_ready'
);


ALTER TYPE "status"."incident_stage" OWNER TO "postgres";


CREATE TYPE "status"."maintenance_status" AS ENUM (
    'scheduled',
    'in_progress',
    'completed',
    'cancelled'
);


ALTER TYPE "status"."maintenance_status" OWNER TO "postgres";


CREATE TYPE "status"."status_state" AS ENUM (
    'operational',
    'degraded_performance',
    'partial_outage',
    'major_outage',
    'maintenance'
);


ALTER TYPE "status"."status_state" OWNER TO "postgres";


CREATE TYPE "status"."status_window" AS ENUM (
    '24h',
    '7d',
    '30d',
    '90d',
    '1y'
);


ALTER TYPE "status"."status_window" OWNER TO "postgres";


CREATE TYPE "wallet"."charge_status_enum" AS ENUM (
    'reserved',
    'settled',
    'released'
);


ALTER TYPE "wallet"."charge_status_enum" OWNER TO "postgres";


CREATE TYPE "wallet"."transaction_type_enum" AS ENUM (
    'deposit',
    'spend',
    'refund',
    'sponsorship_deposit',
    'sponsorship_payout',
    'platform_fee',
    'adjustment'
);


ALTER TYPE "wallet"."transaction_type_enum" OWNER TO "postgres";


CREATE TYPE "xp"."contribution_context_enum" AS ENUM (
    'main_project',
    'community_plugin',
    'documentation',
    'infrastructure'
);


ALTER TYPE "xp"."contribution_context_enum" OWNER TO "postgres";


CREATE TYPE "xp"."difficulty_enum" AS ENUM (
    'easy',
    'standard',
    'hard',
    'legendary'
);


ALTER TYPE "xp"."difficulty_enum" OWNER TO "postgres";


CREATE TYPE "xp"."source_enum" AS ENUM (
    'system',
    'ai',
    'battle',
    'challenge',
    'daily',
    'referral',
    'social',
    'content',
    'other',
    'contribution'
);


ALTER TYPE "xp"."source_enum" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."can_manage_ai_lenser"("p_ai_lenser_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  );
$$;


ALTER FUNCTION "agents"."can_manage_ai_lenser"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."can_manage_ai_lenser"("p_ai_lenser_id" "uuid") IS 'Owner/co-owner authorization helper for control-room tables and RPCs.';



CREATE OR REPLACE FUNCTION "agents"."check_model_binding_single_provider"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'ai', 'public'
    AS $$
DECLARE
  v_binding_mode    text;
  v_new_provider    text;
  v_existing_provider text;
BEGIN
  -- Resolve model_binding_mode for this agent
  SELECT pol.model_binding_mode::text INTO v_binding_mode
  FROM agents.policies pol
  WHERE pol.ai_lenser_id = NEW.ai_lenser_id
  LIMIT 1;

  -- If no policy row yet or mode is not 'single', allow
  IF v_binding_mode IS NULL OR v_binding_mode <> 'single' THEN
    RETURN NEW;
  END IF;

  -- Resolve provider key of the incoming model
  SELECT p.key INTO v_new_provider
  FROM ai.models m
  JOIN ai.providers p ON p.id = m.provider_id
  WHERE m.id = NEW.model_id;

  IF v_new_provider IS NULL THEN
    RAISE EXCEPTION 'Model % not found in ai.models or its provider is missing.', NEW.model_id;
  END IF;

  -- Find the provider key of any existing binding (excluding self on UPDATE)
  SELECT DISTINCT p.key INTO v_existing_provider
  FROM agents.model_bindings mb
  JOIN ai.models m ON m.id = mb.model_id
  JOIN ai.providers p ON p.id = m.provider_id
  WHERE mb.ai_lenser_id = NEW.ai_lenser_id
    AND mb.id IS DISTINCT FROM NEW.id
  LIMIT 1;

  IF v_existing_provider IS NOT NULL AND v_existing_provider <> v_new_provider THEN
    RAISE EXCEPTION
      'Provider mismatch: agent is in single-provider mode (current: %). Cannot add model from provider "%" — remove existing bindings first.',
      v_existing_provider, v_new_provider;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."check_model_binding_single_provider"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_agent_action"("p_ai_lenser_id" "uuid", "p_action_type" "text", "p_context_type" "text" DEFAULT NULL::"text", "p_context_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_policy agents.policies%ROWTYPE;
  v_quota  agents.quota_snapshots%ROWTYPE;
  v_result text := 'success';
BEGIN
  -- Load policy
  SELECT * INTO v_policy
  FROM agents.policies WHERE ai_lenser_id = p_ai_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'no_policy_for_agent: %', p_ai_lenser_id;
  END IF;

  -- Policy gate
  IF p_action_type = 'join_battle'   AND NOT v_policy.can_join_battles   THEN v_result := 'blocked_by_policy'; END IF;
  IF p_action_type = 'cast_vote'     AND NOT v_policy.can_vote            THEN v_result := 'blocked_by_policy'; END IF;
  IF p_action_type = 'create_battle' AND NOT v_policy.can_create_battles  THEN v_result := 'blocked_by_policy'; END IF;

  -- Quota check (only if policy allows)
  IF v_result = 'success' THEN
    -- Lazily create today's quota row
    INSERT INTO agents.quota_snapshots (ai_lenser_id, period_date)
    VALUES (p_ai_lenser_id, CURRENT_DATE)
    ON CONFLICT (ai_lenser_id, period_date) DO NOTHING;

    SELECT * INTO v_quota
    FROM agents.quota_snapshots
    WHERE ai_lenser_id = p_ai_lenser_id AND period_date = CURRENT_DATE;

    IF p_action_type = 'join_battle'
       AND v_policy.max_daily_battles > 0
       AND v_quota.battles_used >= v_policy.max_daily_battles
    THEN
      v_result := 'throttled';
    END IF;

    IF p_action_type = 'cast_vote'
       AND v_policy.max_daily_votes > 0
       AND v_quota.votes_used >= v_policy.max_daily_votes
    THEN
      v_result := 'throttled';
    END IF;
  END IF;

  -- Always log (including blocked and throttled outcomes)
  INSERT INTO agents.action_logs (
    ai_lenser_id, action_type, context_ref_type, context_ref_id, result, metadata
  )
  VALUES (
    p_ai_lenser_id, p_action_type, p_context_type, p_context_id, v_result, p_metadata
  );

  -- Update quota counters only on success
  IF v_result = 'success' THEN
    UPDATE agents.quota_snapshots SET
      battles_used  = battles_used  + CASE WHEN p_action_type = 'join_battle' THEN 1 ELSE 0 END,
      votes_used    = votes_used    + CASE WHEN p_action_type = 'cast_vote'   THEN 1 ELSE 0 END,
      updated_at    = now()
    WHERE ai_lenser_id = p_ai_lenser_id AND period_date = CURRENT_DATE;
  END IF;

  RETURN jsonb_build_object(
    'result',  v_result,
    'action',  p_action_type,
    'agent_id', p_ai_lenser_id
  );
END;
$$;


ALTER FUNCTION "agents"."fn_agent_action"("p_ai_lenser_id" "uuid", "p_action_type" "text", "p_context_type" "text", "p_context_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_agent_action"("p_ai_lenser_id" "uuid", "p_action_type" "text", "p_context_type" "text", "p_context_id" "uuid", "p_metadata" "jsonb") IS 'Single entry point for all AI Lenser autonomous actions. Evaluates policy constraints, daily quota limits, logs the outcome, and increments quota counters on success. Returns: {result: success|blocked_by_policy|throttled|failed, action, agent_id}.';



CREATE OR REPLACE FUNCTION "agents"."fn_apply_standing_approval"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_gate_kind text;
BEGIN
  IF NEW.approval_status IS DISTINCT FROM 'pending' THEN
    RETURN NEW;
  END IF;

  v_gate_kind := NEW.metadata->>'gate_kind';

  IF agents.fn_has_standing_approval(NEW.ai_lenser_id, NEW.workflow_id, v_gate_kind) THEN
    NEW.approval_status := 'approved';
    -- 'queued' is the canonical post-approval status used elsewhere in the
    -- pipeline (see fn_dispatch_scheduled_workflows). Only flip when the
    -- caller left it at the blocked-on-approval default.
    IF NEW.status = 'blocked' THEN
      NEW.status := 'queued';
    END IF;
    NEW.metadata := COALESCE(NEW.metadata, '{}'::jsonb)
      || jsonb_build_object('standing_approval_used', true);
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."fn_apply_standing_approval"() OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_apply_standing_approval"() IS 'Phase Q2: BEFORE INSERT trigger on agents.team_runs. Promotes pending rows to approved when agents.fn_has_standing_approval() matches; sets metadata.standing_approval_used=true and unblocks the run.';



CREATE OR REPLACE FUNCTION "agents"."fn_build_lenser_prompt_context"("p_ai_lenser_id" "uuid", "p_scope" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20) RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_limit  integer := LEAST(GREATEST(COALESCE(p_limit, 20), 1), 50);
  v_rows   RECORD;
  v_lines  text[]  := ARRAY[]::text[];
BEGIN
  FOR v_rows IN
    SELECT scope, source, content
    FROM   agents.memories
    WHERE  ai_lenser_id = p_ai_lenser_id
      AND  is_redacted  = FALSE
      AND  (p_scope IS NULL OR scope = p_scope)
      AND  (expires_at IS NULL OR expires_at > now())
    ORDER  BY created_at DESC
    LIMIT  v_limit
  LOOP
    v_lines := array_append(
      v_lines,
      format('- (%s/%s) %s', v_rows.scope, v_rows.source, v_rows.content)
    );
  END LOOP;

  IF array_length(v_lines, 1) IS NULL THEN
    RETURN NULL;
  END IF;

  RETURN '## Context Memory' || E'\n' || array_to_string(v_lines, E'\n') || E'\n\n';
END;
$$;


ALTER FUNCTION "agents"."fn_build_lenser_prompt_context"("p_ai_lenser_id" "uuid", "p_scope" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_build_lenser_prompt_context"("p_ai_lenser_id" "uuid", "p_scope" "text", "p_limit" integer) IS 'Phase AJ: Returns a formatted memory context block prepended to lens templates during server-side workflow execution. Returns NULL when no eligible entries exist. SECURITY DEFINER — caller must ensure p_ai_lenser_id is authorized.';



CREATE OR REPLACE FUNCTION "agents"."fn_bulk_approve"("p_filters" "jsonb" DEFAULT '{}'::"jsonb") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_status      text       := COALESCE(p_filters->>'status', 'pending');
  v_since_text  text       := COALESCE(p_filters->>'since', '1 hour');
  v_since       interval;
  v_workflow_id uuid       := NULLIF(p_filters->>'workflow_id', '')::uuid;
  v_cutoff      timestamptz;
  v_actor       uuid;
  v_now         timestamptz := now();
  v_approved    integer    := 0;
  r             RECORD;
BEGIN
  v_actor := lensers.get_auth_human_lenser_id();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  BEGIN
    v_since := v_since_text::interval;
  EXCEPTION WHEN OTHERS THEN
    RAISE EXCEPTION 'Invalid since="%". Expected a Postgres interval string.', v_since_text
      USING ERRCODE = '22023';
  END;

  v_cutoff := v_now - v_since;

  FOR r IN
    SELECT tr.id, tr.ai_lenser_id, tr.workflow_run_id, tr.workflow_id, tr.metadata
    FROM   agents.team_runs tr
    WHERE  tr.approval_status = v_status
      AND  tr.created_at      > v_cutoff
      AND  (v_workflow_id IS NULL OR tr.workflow_id = v_workflow_id)
      AND  agents.can_manage_ai_lenser(tr.ai_lenser_id)
    FOR UPDATE SKIP LOCKED
  LOOP
    -- Defence-in-depth: only flip if still pending. Skip silently otherwise.
    UPDATE agents.team_runs
       SET approval_status = 'approved',
           status          = 'queued',
           metadata        = COALESCE(metadata, '{}'::jsonb)
                               || jsonb_build_object(
                                    'decision_at',           v_now,
                                    'decision_by_lenser_id', v_actor,
                                    'decision_source',       'bulk_approve'
                                  ),
           updated_at      = v_now
     WHERE id = r.id
       AND approval_status = 'pending';

    IF NOT FOUND THEN
      CONTINUE;
    END IF;

    IF r.workflow_run_id IS NOT NULL THEN
      UPDATE lenses.workflow_runs
         SET status = 'pending'
       WHERE id = r.workflow_run_id
         AND status IN ('pending');
    END IF;

    INSERT INTO agents.action_logs (
      ai_lenser_id, action_type, context_ref_type, context_ref_id, result, metadata
    )
    VALUES (
      r.ai_lenser_id,
      'approval_granted_bulk',
      'team_run',
      r.id,
      'success',
      jsonb_build_object(
        'workflow_id',          r.workflow_id,
        'workflow_run_id',      r.workflow_run_id,
        'decided_by_lenser_id', v_actor,
        'filters',              p_filters,
        'decided_at',           v_now
      )
    );

    INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
    VALUES (
      r.id,
      'approval_granted',
      jsonb_build_object(
        'decision',             'approved',
        'decision_source',      'bulk_approve',
        'decided_by_lenser_id', v_actor,
        'workflow_run_id',      r.workflow_run_id
      ),
      v_now
    );

    v_approved := v_approved + 1;
  END LOOP;

  RETURN v_approved;
END;
$$;


ALTER FUNCTION "agents"."fn_bulk_approve"("p_filters" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_bulk_approve"("p_filters" "jsonb") IS 'Phase Y3: bulk-approve pending team_runs owned by the caller, filtered by jsonb {status, since, workflow_id}. Returns the count of rows transitioned. Rows the caller does not own (per agents.can_manage_ai_lenser) are silently skipped. Idempotent: only approval_status=pending rows are flipped.';



CREATE OR REPLACE FUNCTION "agents"."fn_claim_team_run"("p_worker_id" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "workflow_id" "uuid", "workflow_run_id" "uuid", "metadata" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_claim_id uuid;
BEGIN
  -- Take ONE queued team run with SKIP LOCKED so concurrent workers don't
  -- contend. Mirrors execution.fn_poll_async_run.
  SELECT tr.id INTO v_claim_id
  FROM agents.team_runs tr
  WHERE tr.status = 'queued'
    AND tr.approval_status IN ('not_required', 'approved')
  ORDER BY tr.created_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1;

  IF v_claim_id IS NULL THEN
    RETURN;
  END IF;

  UPDATE agents.team_runs tr
  SET    status     = 'running',
         started_at = COALESCE(tr.started_at, now()),
         updated_at = now(),
         metadata   = tr.metadata || jsonb_build_object(
           'claimed_by', COALESCE(p_worker_id, 'unknown'),
           'claimed_at', now()
         )
  WHERE  tr.id = v_claim_id;

  INSERT INTO agents.agent_run_events (team_run_id, event_type, payload)
  VALUES (
    v_claim_id,
    'dispatch_started',
    jsonb_build_object('worker_id', COALESCE(p_worker_id, 'unknown'))
  );

  RETURN QUERY
    SELECT tr.id, tr.ai_lenser_id, tr.workflow_id, tr.workflow_run_id, tr.metadata
    FROM agents.team_runs tr
    WHERE tr.id = v_claim_id;
END;
$$;


ALTER FUNCTION "agents"."fn_claim_team_run"("p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_claim_team_run"("p_worker_id" "text") IS 'D10 fix: disambiguates id by renaming local var to v_claim_id and qualifying WHERE predicates with the table alias. Service-role only.';



CREATE OR REPLACE FUNCTION "agents"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'audit', 'public'
    AS $$
DECLARE
  v_profile_id uuid;
  v_lenser_id  uuid;
BEGIN
  -- Validate owner exists and is an active human lenser
  IF NOT EXISTS (
    SELECT 1 FROM lensers.profiles
    WHERE id     = p_owner_lenser_id
      AND type   = 'human'
      AND status = 'active'
  ) THEN
    RAISE EXCEPTION 'owner_must_be_active_human_lenser';
  END IF;

  -- Enforce max 5 AI agents per owner
  IF (
    SELECT COUNT(*)
    FROM agents.ownerships
    WHERE owner_lenser_id = p_owner_lenser_id
      AND role            = 'owner'
      AND revoked_at      IS NULL
  ) >= 5 THEN
    RAISE EXCEPTION 'Maximum 5 AI agents allowed per lenser'
      USING ERRCODE = 'P0004';
  END IF;

  -- 1. Create lensers.profiles entry (type = 'ai')
  --    Explicitly set user_id = NULL: AI lensers have no auth.users row.
  --    Without this, the column DEFAULT (auth.uid()) would copy the caller's
  --    UID and violate the profiles_user_id_unique constraint.
  INSERT INTO lensers.profiles (
    user_id, handle, display_name, type, ai_model_id,
    status, visibility, onboarding_step
  )
  VALUES (
    NULL, p_handle, p_display_name, 'ai', p_ai_model_id,
    'active', 'public', 1
  )
  RETURNING id INTO v_profile_id;

  -- 2. Create extension record
  INSERT INTO agents.ai_lensers (profile_id)
  VALUES (v_profile_id)
  RETURNING id INTO v_lenser_id;

  -- 3. Grant primary ownership with full default scope
  INSERT INTO agents.ownerships (
    ai_lenser_id, owner_lenser_id, role, permission_scope
  )
  VALUES (
    v_lenser_id, p_owner_lenser_id, 'owner',
    ARRAY['join_battles', 'vote', 'create_battles', 'spend_credits']
  );

  -- 4. Bootstrap default policy (all capabilities disabled — owner must enable)
  INSERT INTO agents.policies (ai_lenser_id)
  VALUES (v_lenser_id);

  -- 5. Record attestation
  INSERT INTO audit.attestations (
    entity_type, entity_id, attested_by, attestation_type, proof_payload
  )
  VALUES (
    'ai_lenser', v_lenser_id, p_owner_lenser_id, 'agent_created',
    jsonb_build_object(
      'handle',    p_handle,
      'owner_id',  p_owner_lenser_id,
      'model_id',  p_ai_model_id
    )
  );

  RETURN jsonb_build_object(
    'profile_id',   v_profile_id,
    'ai_lenser_id', v_lenser_id,
    'status',       'created'
  );
END;
$$;


ALTER FUNCTION "agents"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") IS 'Transactionally creates the entire Agent aggregate: lensers.profiles, agents.ai_lensers, agents.ownerships (primary owner), agents.policies (all-disabled defaults), and audit.attestations. Rolls back atomically on any failure. This is the only supported way to create an AI Lenser.';



CREATE OR REPLACE FUNCTION "agents"."fn_enforce_team_messages_cap"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_count integer;
  v_cap   constant integer := 1000;
BEGIN
  SELECT count(*) INTO v_count
  FROM   agents.team_messages
  WHERE  team_run_id = NEW.team_run_id;

  IF v_count >= v_cap THEN
    RAISE EXCEPTION 'team_messages_cap_exceeded: team_run % already has % messages (cap %)',
      NEW.team_run_id, v_count, v_cap
      USING ERRCODE = '54000';
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."fn_enforce_team_messages_cap"() OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_enforce_team_messages_cap"() IS 'Phase X1: BEFORE INSERT guard enforcing 1000 msgs/team_run. Operators needing higher ceilings should bump the constant or add a per-team_run override field; counted via simple count(*) — fine because the partial index on (team_run_id, occurred_at DESC) covers it.';



CREATE OR REPLACE FUNCTION "agents"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid    UUID := auth.uid();
  v_count  INT;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_command_ids IS NULL OR cardinality(p_command_ids) = 0 THEN
    RETURN 0;
  END IF;
  IF cardinality(p_command_ids) > 100 THEN
    RAISE EXCEPTION 'too_many_ids' USING ERRCODE = '22023';
  END IF;

  WITH owned AS (
    SELECT gc.id
      FROM agents.gateway_commands gc
      JOIN agents.gateway_devices d ON d.device_id = gc.device_id
     WHERE d.owner_id = v_uid
       AND gc.id = ANY(p_command_ids)
       AND gc.acked_at IS NULL
  )
  UPDATE agents.gateway_commands gc
     SET acked_at = now()
    FROM owned
   WHERE gc.id = owned.id;

  GET DIAGNOSTICS v_count = ROW_COUNT;
  RETURN v_count;
END $$;


ALTER FUNCTION "agents"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_gateway_approve_device"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid UUID := auth.uid();
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.gateway_devices
     SET approved_at = now(),
         revoked_at  = NULL,
         kill_switch = false
   WHERE device_id = p_device_id
     AND owner_id  = v_uid;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'device_not_owned' USING ERRCODE = '42501';
  END IF;
END $$;


ALTER FUNCTION "agents"."fn_gateway_approve_device"("p_device_id" "uuid") OWNER TO "postgres";

SET default_tablespace = '';

SET default_table_access_method = "heap";


CREATE TABLE IF NOT EXISTS "agents"."gateway_commands" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "device_id" "uuid" NOT NULL,
    "command_type" "text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "claimed_at" timestamp with time zone,
    "acked_at" timestamp with time zone,
    "envelope_sig" "text",
    "envelope_nonce" "text",
    CONSTRAINT "gateway_commands_command_type_check" CHECK ((("char_length"("command_type") >= 1) AND ("char_length"("command_type") <= 64))),
    CONSTRAINT "gateway_commands_envelope_nonce_check" CHECK ((("envelope_nonce" IS NULL) OR (("char_length"("envelope_nonce") >= 16) AND ("char_length"("envelope_nonce") <= 128)))),
    CONSTRAINT "gateway_commands_envelope_sig_check" CHECK ((("envelope_sig" IS NULL) OR (("char_length"("envelope_sig") >= 16) AND ("char_length"("envelope_sig") <= 512))))
);


ALTER TABLE "agents"."gateway_commands" OWNER TO "postgres";


COMMENT ON TABLE "agents"."gateway_commands" IS 'Phase AV: outbox of commands queued by the cloud for a specific gateway device. Daemons pull via fn_gateway_claim_commands and confirm via fn_gateway_ack_commands.';



COMMENT ON COLUMN "agents"."gateway_commands"."envelope_sig" IS 'Phase BG: base64url Ed25519 signature over the canonical JCS encoding of {id, device_id, command_type, payload, created_at, envelope_nonce}. The daemon refuses commands whose signature does not verify against the cloud signing key.';



COMMENT ON COLUMN "agents"."gateway_commands"."envelope_nonce" IS 'Phase BG: random 128-bit nonce included in the signed envelope so attempted replays of an old (still-valid signature) command can be detected by the daemon.';



CREATE OR REPLACE FUNCTION "agents"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS SETOF "agents"."gateway_commands"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid     UUID := auth.uid();
  v_capped  INT  := LEAST(GREATEST(COALESCE(p_limit, 10), 1), 50);
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF NOT EXISTS (
    SELECT 1 FROM agents.gateway_devices
     WHERE device_id = p_device_id AND owner_id = v_uid
  ) THEN
    RAISE EXCEPTION 'device_not_owned' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  WITH claimable AS (
    SELECT id
      FROM agents.gateway_commands
     WHERE device_id  = p_device_id
       AND claimed_at IS NULL
     ORDER BY created_at ASC
     LIMIT v_capped
     FOR UPDATE SKIP LOCKED
  )
  UPDATE agents.gateway_commands gc
     SET claimed_at = now()
    FROM claimable
   WHERE gc.id = claimable.id
   RETURNING gc.*;
END $$;


ALTER FUNCTION "agents"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) IS 'Deprecated since Phase BG. Use fn_gateway_claim_commands_v2 instead — it returns the envelope_sig + envelope_nonce columns the daemon needs to verify a command before dispatching it.';



CREATE OR REPLACE FUNCTION "agents"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS TABLE("id" "uuid", "device_id" "uuid", "command_type" "text", "payload" "jsonb", "created_at" timestamp with time zone, "claimed_at" timestamp with time zone, "acked_at" timestamp with time zone, "envelope_sig" "text", "envelope_nonce" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid    UUID := auth.uid();
  v_capped INT  := LEAST(GREATEST(COALESCE(p_limit, 10), 1), 50);
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF NOT EXISTS (
    SELECT 1
    FROM agents.gateway_devices d
    WHERE d.device_id = p_device_id
      AND d.owner_id = v_uid
  ) THEN
    RAISE EXCEPTION 'device_not_owned' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  WITH claimable AS (
    SELECT gc.id
      FROM agents.gateway_commands gc
     WHERE gc.device_id  = p_device_id
       AND gc.claimed_at IS NULL
     ORDER BY gc.created_at ASC
     LIMIT v_capped
     FOR UPDATE SKIP LOCKED
  )
  UPDATE agents.gateway_commands gc
     SET claimed_at = now()
    FROM claimable
   WHERE gc.id = claimable.id
   RETURNING gc.id, gc.device_id, gc.command_type, gc.payload, gc.created_at,
             gc.claimed_at, gc.acked_at, gc.envelope_sig, gc.envelope_nonce;
END $$;


ALTER FUNCTION "agents"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text" DEFAULT NULL::"text", "p_daemon_version" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid          UUID := auth.uid();
  v_row          agents.gateway_devices%ROWTYPE;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_public_key IS NULL OR char_length(p_public_key) < 32 THEN
    RAISE EXCEPTION 'public_key_required' USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.gateway_devices (
    device_id, owner_id, hostname, public_key, daemon_version, last_seen_at
  ) VALUES (
    p_device_id, v_uid, p_hostname, p_public_key, p_daemon_version, now()
  )
  ON CONFLICT (device_id) DO UPDATE
    SET hostname       = COALESCE(EXCLUDED.hostname,       agents.gateway_devices.hostname),
        daemon_version = COALESCE(EXCLUDED.daemon_version, agents.gateway_devices.daemon_version),
        last_seen_at   = now()
    -- public_key is NOT updated on heartbeat; use fn_gateway_rotate_key for explicit rotation.
    WHERE agents.gateway_devices.owner_id = v_uid
  RETURNING * INTO v_row;

  IF v_row.device_id IS NULL THEN
    -- ON CONFLICT collided with a row owned by someone else.
    RAISE EXCEPTION 'device_not_owned' USING ERRCODE = '42501';
  END IF;

  RETURN jsonb_build_object(
    'approved',    v_row.approved_at IS NOT NULL AND v_row.revoked_at IS NULL,
    'kill_switch', COALESCE(v_row.kill_switch, false)
  );
END $$;


ALTER FUNCTION "agents"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_gateway_revoke_device"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid UUID := auth.uid();
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.gateway_devices
     SET revoked_at  = now(),
         kill_switch = true
   WHERE device_id = p_device_id
     AND owner_id  = v_uid;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'device_not_owned' USING ERRCODE = '42501';
  END IF;
END $$;


ALTER FUNCTION "agents"."fn_gateway_revoke_device"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_get_agent_effective_provider"("p_ai_lenser_id" "uuid") RETURNS "text"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'ai', 'public'
    AS $$
  SELECT DISTINCT prov.key
  FROM agents.model_bindings mb
  JOIN ai.models m ON m.id = mb.model_id
  JOIN ai.providers prov ON prov.id = m.provider_id
  WHERE mb.ai_lenser_id = p_ai_lenser_id
  LIMIT 1;
$$;


ALTER FUNCTION "agents"."fn_get_agent_effective_provider"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_get_agent_effective_provider"("p_ai_lenser_id" "uuid") IS 'Returns the provider key for an agent''s existing model bindings, or NULL if none. Used to enforce single-provider mode in the UI.';



CREATE OR REPLACE FUNCTION "agents"."fn_get_team_member_role"("p_team_run_id" "uuid", "p_agent_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_role          text;
  v_ai_lenser_id  uuid;
  v_is_service    boolean := (current_setting('request.jwt.claim.role', true) = 'service_role');
BEGIN
  SELECT tr.ai_lenser_id
    INTO v_ai_lenser_id
    FROM agents.team_runs tr
   WHERE tr.id = p_team_run_id;

  IF v_ai_lenser_id IS NULL THEN
    -- Unknown team_run — do not distinguish from "not yours".
    RETURN 'operator';
  END IF;

  -- Service role keeps full access for the execution engine.
  IF NOT v_is_service AND NOT agents.can_manage_ai_lenser(v_ai_lenser_id) THEN
    RETURN 'operator';
  END IF;

  SELECT tm.role
    INTO v_role
    FROM agents.team_runs    tr
    JOIN agents.team_members tm
      ON tm.team_id  = tr.team_id
     AND tm.agent_id = p_agent_id
   WHERE tr.id = p_team_run_id
   LIMIT 1;

  RETURN COALESCE(v_role, 'operator');
END;
$$;


ALTER FUNCTION "agents"."fn_get_team_member_role"("p_team_run_id" "uuid", "p_agent_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_get_team_member_role"("p_team_run_id" "uuid", "p_agent_id" "uuid") IS 'Phase X4 (hardened): returns role only when caller owns the team_run or is service_role. Strangers probing arbitrary IDs receive ''operator'' and learn nothing about cross-tenant team membership.';



CREATE OR REPLACE FUNCTION "agents"."fn_has_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM   agents.standing_approvals sa
    WHERE  sa.ai_lenser_id = p_ai_lenser_id
      AND  sa.revoked_at IS NULL
      AND  (sa.expires_at IS NULL OR sa.expires_at > now())
      AND  (
        (sa.workflow_id IS NOT NULL AND sa.workflow_id = p_workflow_id)
        OR (sa.gate_kind IS NOT NULL AND sa.gate_kind  = p_gate_kind)
      )
  );
$$;


ALTER FUNCTION "agents"."fn_has_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_has_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text") IS 'Phase Q2: returns true when a non-revoked, non-expired standing approval exists for (ai_lenser_id, workflow_id) or (ai_lenser_id, gate_kind).';



CREATE OR REPLACE FUNCTION "agents"."fn_list_gateway_devices"("p_limit" integer DEFAULT 50) RETURNS TABLE("device_id" "uuid", "hostname" "text", "daemon_version" "text", "last_seen_at" timestamp with time zone, "approved_at" timestamp with time zone, "revoked_at" timestamp with time zone, "kill_switch" boolean, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
DECLARE
  v_uid    UUID := auth.uid();
  v_capped INT  := LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
    SELECT d.device_id, d.hostname, d.daemon_version, d.last_seen_at,
           d.approved_at, d.revoked_at, d.kill_switch, d.created_at
      FROM agents.gateway_devices d
     WHERE d.owner_id = v_uid
     ORDER BY d.last_seen_at DESC NULLS LAST,
              d.created_at DESC
     LIMIT v_capped;
END $$;


ALTER FUNCTION "agents"."fn_list_gateway_devices"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_merge_shared_scratchpad"("p_team_run_id" "uuid", "p_patch" "jsonb", "p_expected_version" integer) RETURNS "jsonb"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_current_version int;
  v_new_value       jsonb;
  v_new_version     int;
BEGIN
  IF p_team_run_id IS NULL OR p_patch IS NULL OR p_expected_version IS NULL THEN
    RAISE EXCEPTION 'arguments required: team_run_id, patch, expected_version'
      USING ERRCODE = '22023';
  END IF;

  -- Atomic check-and-update: predicate on shared_scratchpad_version
  -- guards concurrent writers.
  UPDATE agents.team_runs
     SET shared_scratchpad         = shared_scratchpad || p_patch,
         shared_scratchpad_version = shared_scratchpad_version + 1,
         updated_at                = now()
   WHERE id                        = p_team_run_id
     AND shared_scratchpad_version = p_expected_version
   RETURNING shared_scratchpad, shared_scratchpad_version
        INTO v_new_value, v_new_version;

  IF NOT FOUND THEN
    -- Either the row does not exist, RLS hid it, or version drifted.
    SELECT shared_scratchpad_version
      INTO v_current_version
      FROM agents.team_runs
     WHERE id = p_team_run_id;

    RAISE EXCEPTION 'scratchpad_version_conflict: expected % current %',
      p_expected_version, v_current_version
      USING ERRCODE = '40001';
  END IF;

  RETURN jsonb_build_object(
    'shared_scratchpad',         v_new_value,
    'shared_scratchpad_version', v_new_version
  );
END;
$$;


ALTER FUNCTION "agents"."fn_merge_shared_scratchpad"("p_team_run_id" "uuid", "p_patch" "jsonb", "p_expected_version" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_merge_shared_scratchpad"("p_team_run_id" "uuid", "p_patch" "jsonb", "p_expected_version" integer) IS 'Phase X3: optimistic-lock merge into team_runs.shared_scratchpad. Raises 40001 (scratchpad_version_conflict) if expected version drifted. SECURITY INVOKER: existing RLS on agents.team_runs gates the UPDATE.';



CREATE OR REPLACE FUNCTION "agents"."fn_node_requires_review"("p_team_run_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_exists        boolean;
  v_ai_lenser_id  uuid;
  v_is_service    boolean := (current_setting('request.jwt.claim.role', true) = 'service_role');
BEGIN
  SELECT tr.ai_lenser_id
    INTO v_ai_lenser_id
    FROM agents.team_runs tr
   WHERE tr.id = p_team_run_id;

  IF v_ai_lenser_id IS NULL THEN
    RETURN false;
  END IF;

  IF NOT v_is_service AND NOT agents.can_manage_ai_lenser(v_ai_lenser_id) THEN
    RETURN false;
  END IF;

  SELECT EXISTS (
    SELECT 1
    FROM   agents.team_runs    tr
    JOIN   agents.team_members tm ON tm.team_id = tr.team_id
    WHERE  tr.id        = p_team_run_id
      AND  tm.role      = 'reviewer'
      AND  tm.is_active = true
  )
  INTO v_exists;

  RETURN COALESCE(v_exists, false);
END;
$$;


ALTER FUNCTION "agents"."fn_node_requires_review"("p_team_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_node_requires_review"("p_team_run_id" "uuid") IS 'Phase X4 (hardened): true only when caller owns the team_run (or is service_role) AND the team has an active reviewer. Returns false to strangers regardless of true state.';



CREATE OR REPLACE FUNCTION "agents"."fn_notify_approval_pending"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'audit', 'public'
    AS $$
DECLARE
  v_url     text;
  v_payload jsonb;
BEGIN
  IF NEW.approval_status IS DISTINCT FROM 'pending' THEN
    RETURN NEW;
  END IF;

  v_url := NULLIF(current_setting('app.approval_webhook_url', true), '');
  IF v_url IS NULL THEN
    RETURN NEW;
  END IF;

  v_payload := jsonb_build_object(
    'webhook_version',         1,
    'event',                   'approval_pending',
    'team_run_id',             NEW.id,
    'ai_lenser_id',            NEW.ai_lenser_id,
    'team_id',                 NEW.team_id,
    'workflow_id',             NEW.workflow_id,
    'workflow_run_id',         NEW.workflow_run_id,
    'workflow_assignment_id',  NEW.workflow_assignment_id,
    'gate_kind',               NEW.metadata->>'gate_kind',
    'requested_action',        NEW.metadata->>'requested_action',
    'pending_since',           to_char(COALESCE(NEW.created_at, now()) AT TIME ZONE 'UTC',
                                       'YYYY-MM-DD"T"HH24:MI:SS"Z"')
  );

  INSERT INTO audit.webhook_outbox (event_type, payload, target_url)
  VALUES ('approval_pending', v_payload, v_url);

  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."fn_notify_approval_pending"() OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_notify_approval_pending"() IS 'Phase P3: AFTER INSERT trigger on agents.team_runs. Enqueues an approval_pending webhook row in audit.webhook_outbox when approval_status=pending and app.approval_webhook_url is set. The dispatcher (audit.fn_dispatch_webhook_outbox) handles signing, retries, and POST.';



CREATE OR REPLACE FUNCTION "agents"."fn_purge_stale_blocked_team_runs"("p_max_age" interval DEFAULT '30 days'::interval) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_cutoff timestamptz := now() - GREATEST(p_max_age, interval '1 hour');
  v_purged integer := 0;
  r RECORD;
BEGIN
  FOR r IN
    SELECT id
    FROM   agents.team_runs
    WHERE  status          = 'blocked'
      AND  approval_status = 'pending'
      AND  created_at     <  v_cutoff
    FOR UPDATE SKIP LOCKED
    LIMIT  500   -- bounded sweep; cron re-runs daily
  LOOP
    UPDATE agents.team_runs
    SET    status          = 'cancelled',
           approval_status = 'timed_out',
           completed_at    = now(),
           updated_at      = now()
    WHERE  id = r.id;

    INSERT INTO agents.agent_run_events (team_run_id, event_type, payload)
    VALUES (r.id, 'approval_timed_out',
            jsonb_build_object('reason', 'stale_blocked_purge',
                               'cutoff', v_cutoff));

    v_purged := v_purged + 1;
  END LOOP;

  RETURN v_purged;
END;
$$;


ALTER FUNCTION "agents"."fn_purge_stale_blocked_team_runs"("p_max_age" interval) OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_purge_stale_blocked_team_runs"("p_max_age" interval) IS 'D6: daily age-out for blocked team_runs whose approval never arrived. Transitions to status=cancelled, approval_status=timed_out, emits approval_timed_out event. Bounded sweep of 500 per call.';



CREATE OR REPLACE FUNCTION "agents"."fn_send_team_message"("p_team_run_id" "uuid", "p_from_agent_id" "uuid", "p_kind" "text", "p_to_agent_id" "uuid" DEFAULT NULL::"uuid", "p_payload" "jsonb" DEFAULT '{}'::"jsonb", "p_parent_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_id uuid;
BEGIN
  IF p_team_run_id IS NULL THEN
    RAISE EXCEPTION 'p_team_run_id is required' USING ERRCODE = '22023';
  END IF;
  IF p_from_agent_id IS NULL THEN
    RAISE EXCEPTION 'p_from_agent_id is required' USING ERRCODE = '22023';
  END IF;
  IF p_kind NOT IN ('task_request','task_response','info','error') THEN
    RAISE EXCEPTION 'invalid kind: %', p_kind USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.team_messages (
    team_run_id, from_agent_id, to_agent_id, kind, payload, parent_message_id
  )
  VALUES (
    p_team_run_id, p_from_agent_id, p_to_agent_id, p_kind,
    COALESCE(p_payload, '{}'::jsonb), p_parent_id
  )
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "agents"."fn_send_team_message"("p_team_run_id" "uuid", "p_from_agent_id" "uuid", "p_kind" "text", "p_to_agent_id" "uuid", "p_payload" "jsonb", "p_parent_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_send_team_message"("p_team_run_id" "uuid", "p_from_agent_id" "uuid", "p_kind" "text", "p_to_agent_id" "uuid", "p_payload" "jsonb", "p_parent_id" "uuid") IS 'Phase X1: append a message to the team-run bus. SECURITY INVOKER; RLS on agents.team_messages enforces ownership. Returns the inserted id.';



CREATE OR REPLACE FUNCTION "agents"."fn_start_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_policy" "text" DEFAULT 'auto'::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_team_run_id     uuid;
  v_status          text;
  v_approval_status text;
  v_parent_id       uuid;
  v_parent_depth    integer;
  v_new_depth       integer;
BEGIN
  IF p_ai_lenser_id IS NULL THEN
    RAISE EXCEPTION 'ai_lenser_id is required'
      USING ERRCODE = 'check_violation';
  END IF;

  IF p_policy NOT IN ('auto', 'approval_required', 'forbidden') THEN
    RAISE EXCEPTION 'Unknown delegation policy: %', p_policy
      USING ERRCODE = 'check_violation';
  END IF;

  IF p_policy = 'forbidden' THEN
    RAISE EXCEPTION 'delegation_forbidden'
      USING ERRCODE = 'P0001',
            HINT    = 'Workflow node delegationPolicy=forbidden';
  END IF;

  -- ── D5: recursion-depth cap ──────────────────────────────────────────────
  -- Workers pass the parent team_run id via the inputs envelope:
  --   p_inputs := jsonb_build_object('_parent_team_run_id', <uuid>, ...)
  IF p_inputs IS NOT NULL AND p_inputs ? '_parent_team_run_id' THEN
    BEGIN
      v_parent_id := (p_inputs->>'_parent_team_run_id')::uuid;
    EXCEPTION WHEN OTHERS THEN
      v_parent_id := NULL;
    END;
  END IF;

  IF v_parent_id IS NOT NULL THEN
    SELECT recursion_depth INTO v_parent_depth
    FROM agents.team_runs
    WHERE id = v_parent_id;

    v_new_depth := COALESCE(v_parent_depth, 0) + 1;

    IF v_new_depth > 8 THEN
      RAISE EXCEPTION
        'team_run_recursion_cap_exceeded: depth=% > 8 (parent=%)',
        v_new_depth, v_parent_id
        USING ERRCODE = '54000',
              HINT    = 'D5: agents.fn_start_team_run recursion cap';
    END IF;
  ELSE
    v_new_depth := 0;
  END IF;
  -- ── End D5 ───────────────────────────────────────────────────────────────

  IF p_policy = 'approval_required' THEN
    v_status          := 'blocked';
    v_approval_status := 'pending';
  ELSE
    v_status          := 'queued';
    v_approval_status := 'not_required';
  END IF;

  INSERT INTO agents.team_runs (
    ai_lenser_id, workflow_id, status, approval_status,
    parent_team_run_id, recursion_depth, metadata
  )
  VALUES (
    p_ai_lenser_id, p_workflow_id, v_status, v_approval_status,
    v_parent_id, v_new_depth,
    jsonb_build_object(
      'inputs',            p_inputs - '_parent_team_run_id',
      'origin',            'delegate_to_agent',
      'delegation_policy', p_policy
    )
  )
  RETURNING id INTO v_team_run_id;

  INSERT INTO agents.agent_run_events (team_run_id, event_type, payload)
  VALUES (
    v_team_run_id,
    CASE WHEN p_policy = 'approval_required'
         THEN 'approval_requested'
         ELSE 'dispatch_queued'
    END,
    jsonb_build_object(
      'workflow_id',       p_workflow_id,
      'delegation_policy', p_policy,
      'requires_approval', p_policy = 'approval_required',
      'recursion_depth',   v_new_depth
    )
  );

  RETURN v_team_run_id;
END;
$$;


ALTER FUNCTION "agents"."fn_start_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_inputs" "jsonb", "p_policy" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."fn_start_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_inputs" "jsonb", "p_policy" "text") IS 'Service-role-only team-run dispatch. Enforces D5 recursion cap (≤8) when parent passed via inputs._parent_team_run_id. Maps policy → (status, approval_status). SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "agents"."fn_tools_registry_egress_guard"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'pg_catalog'
    AS $$
BEGIN
  IF NEW.egress_class = 'write' AND NEW.requires_approval IS DISTINCT FROM true THEN
    NEW.requires_approval := true;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."fn_tools_registry_egress_guard"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_trg_notify_agent_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_ai_profile_id UUID;
  v_ai_handle     TEXT;
BEGIN
  -- Only notify for the first ownership row (first 'owner') to avoid firing
  -- again when a co_owner is added later.
  IF EXISTS (
    SELECT 1 FROM agents.ownerships
    WHERE  ai_lenser_id = NEW.ai_lenser_id
      AND  id           != NEW.id
      AND  role         = 'owner'
  ) THEN
    RETURN NEW;
  END IF;

  -- Resolve the AI agent's profile handle for the action URL
  SELECT al.profile_id, p.handle
  INTO   v_ai_profile_id, v_ai_handle
  FROM   agents.ai_lensers al
  JOIN   lensers.profiles p ON p.id = al.profile_id
  WHERE  al.id = NEW.ai_lenser_id;

  PERFORM public.fn_insert_notification(
    NEW.owner_lenser_id,
    'agent_created',
    'Your AI agent has been created',
    'Your agent @' || COALESCE(v_ai_handle, 'unknown') || ' is ready.',
    '/lenser/' || COALESCE(v_ai_handle, '') || '/ag',
    jsonb_build_object(
      'ai_lenser_id',  NEW.ai_lenser_id,
      'ai_profile_id', v_ai_profile_id,
      'ai_handle',     v_ai_handle
    )
    -- p_actor_id = NULL
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_agent_created failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."fn_trg_notify_agent_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_trigger_post_run_evaluations"("p_workflow_id" "uuid", "p_team_run_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_eval RECORD;
BEGIN
  FOR v_eval IN
    SELECT id FROM agents.evaluations
    WHERE target_type = 'workflow'
      AND target_id = p_workflow_id
  LOOP
    PERFORM public.fn_run_evaluation(v_eval.id, NULL);
  END LOOP;
END;
$$;


ALTER FUNCTION "agents"."fn_trigger_post_run_evaluations"("p_workflow_id" "uuid", "p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_updates" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_is_owner boolean;
  v_policy_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  SELECT EXISTS (
    SELECT 1
    FROM agents.ownerships
    WHERE ai_lenser_id = p_ai_lenser_id
      AND owner_lenser_id = v_caller_human_id
      AND role IN ('owner', 'co_owner')
      AND revoked_at IS NULL
  ) INTO v_is_owner;

  IF NOT v_is_owner THEN
    RAISE EXCEPTION 'Only the agent owner or co-owner can update policies'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  SELECT id INTO v_policy_id
  FROM agents.policies
  WHERE ai_lenser_id = p_ai_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'No policy found for this AI lenser'
      USING ERRCODE = 'no_data_found';
  END IF;

  UPDATE agents.policies SET
    can_join_battles        = COALESCE((p_updates ->> 'can_join_battles')::boolean, can_join_battles),
    can_vote                = COALESCE((p_updates ->> 'can_vote')::boolean, can_vote),
    can_create_battles      = COALESCE((p_updates ->> 'can_create_battles')::boolean, can_create_battles),
    can_receive_sponsorship = COALESCE((p_updates ->> 'can_receive_sponsorship')::boolean, can_receive_sponsorship),
    model_binding_mode      = COALESCE(
      CASE WHEN p_updates ? 'model_binding_mode' THEN (p_updates ->> 'model_binding_mode')::text END,
      model_binding_mode
    ),
    max_daily_battles       = COALESCE((p_updates ->> 'max_daily_battles')::integer, max_daily_battles),
    max_daily_votes         = COALESCE((p_updates ->> 'max_daily_votes')::integer, max_daily_votes),
    spending_limit_credits  = COALESCE((p_updates ->> 'spending_limit_credits')::integer, spending_limit_credits),
    is_public_policy        = COALESCE((p_updates ->> 'is_public_policy')::boolean, is_public_policy),
    updated_at              = now()
  WHERE id = v_policy_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id,
    action_type,
    result,
    metadata
  ) VALUES (
    p_ai_lenser_id,
    'policy_updated',
    'success',
    jsonb_build_object('fields', ARRAY(SELECT jsonb_object_keys(p_updates)))
  );

  RETURN jsonb_build_object('updated', true, 'policy_id', v_policy_id);
END;
$$;


ALTER FUNCTION "agents"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_updates" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."fn_workspace_settings_paused_sync"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public', 'extensions'
    AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    -- If exactly one side is set explicitly, mirror to the other.
    -- Both default to false; if both are false on insert, no-op.
    IF NEW.runner_paused IS DISTINCT FROM NEW.agent_paused THEN
      -- Prefer runner_paused if it is non-default and agent_paused is default.
      -- Otherwise prefer agent_paused (legacy callers).
      IF NEW.runner_paused = true AND NEW.agent_paused = false THEN
        NEW.agent_paused := true;
      ELSIF NEW.agent_paused = true AND NEW.runner_paused = false THEN
        NEW.runner_paused := true;
      END IF;
    END IF;
    RETURN NEW;
  END IF;

  IF TG_OP = 'UPDATE' THEN
    -- If only one column changed, mirror it to the other.
    IF NEW.runner_paused IS DISTINCT FROM OLD.runner_paused
       AND NEW.agent_paused = OLD.agent_paused THEN
      NEW.agent_paused := NEW.runner_paused;
    ELSIF NEW.agent_paused IS DISTINCT FROM OLD.agent_paused
          AND NEW.runner_paused = OLD.runner_paused THEN
      NEW.runner_paused := NEW.agent_paused;
    END IF;
    RETURN NEW;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."fn_workspace_settings_paused_sync"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."has_agent_scope"("p_ai_lenser_id" "uuid", "p_scope" "text") RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_role text;
  v_scopes text[];
BEGIN
  SELECT o.role, o.permission_scope
  INTO v_role, v_scopes
  FROM agents.ownerships o
  WHERE o.ai_lenser_id = p_ai_lenser_id
    AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
    AND o.revoked_at IS NULL
  ORDER BY CASE o.role
    WHEN 'owner' THEN 1
    WHEN 'co_owner' THEN 2
    ELSE 3
  END
  LIMIT 1;

  IF v_role IS NULL THEN
    RETURN false;
  END IF;

  IF v_role IN ('owner', 'co_owner') THEN
    RETURN true;
  END IF;

  RETURN COALESCE(v_scopes @> ARRAY[p_scope], false);
END;
$$;


ALTER FUNCTION "agents"."has_agent_scope"("p_ai_lenser_id" "uuid", "p_scope" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "agents"."has_agent_scope"("p_ai_lenser_id" "uuid", "p_scope" "text") IS 'Returns whether the authenticated human Lenser may perform the requested scoped action on an AI Lenser. owner/co_owner bypass scopes; operator must hold the explicit scope.';



CREATE OR REPLACE FUNCTION "agents"."trg_notify_model_binding_changed"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_ai_lenser_id  UUID := COALESCE(NEW.ai_lenser_id, OLD.ai_lenser_id);
  v_ai_profile_id UUID;
  v_model_id      TEXT;
BEGIN
  SELECT profile_id INTO v_ai_profile_id
  FROM   agents.ai_lensers WHERE id = v_ai_lenser_id;

  IF v_ai_profile_id IS NULL THEN RETURN COALESCE(NEW, OLD); END IF;

  v_model_id := COALESCE(NEW.model_id, OLD.model_id)::text;

  PERFORM public.fn_insert_notification(
    v_ai_profile_id,
    'model_binding_changed',
    'Model binding ' || lower(TG_OP),
    NULL,
    '/lenser/' || (SELECT handle FROM lensers.profiles WHERE id = v_ai_profile_id) || '/ag/settings',
    jsonb_build_object(
      'ai_lenser_id', v_ai_lenser_id,
      'model_id',     v_model_id,
      'is_default',   COALESCE(NEW.is_default, false),
      'operation',    lower(TG_OP)
    )
  );

  RETURN COALESCE(NEW, OLD);
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_model_binding_changed failed: %', SQLERRM;
  RETURN COALESCE(NEW, OLD);
END;
$$;


ALTER FUNCTION "agents"."trg_notify_model_binding_changed"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."trg_notify_policy_updated"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_ai_profile_id UUID;
  v_changes       JSONB := '{}';
BEGIN
  SELECT profile_id INTO v_ai_profile_id
  FROM   agents.ai_lensers WHERE id = NEW.ai_lenser_id;

  IF v_ai_profile_id IS NULL THEN RETURN NEW; END IF;

  -- Collect meaningfully changed policy fields
  IF OLD.can_join_battles     IS DISTINCT FROM NEW.can_join_battles     THEN
    v_changes := v_changes || jsonb_build_object('can_join_battles', NEW.can_join_battles);
  END IF;
  IF OLD.can_vote             IS DISTINCT FROM NEW.can_vote             THEN
    v_changes := v_changes || jsonb_build_object('can_vote', NEW.can_vote);
  END IF;
  IF OLD.can_create_battles   IS DISTINCT FROM NEW.can_create_battles   THEN
    v_changes := v_changes || jsonb_build_object('can_create_battles', NEW.can_create_battles);
  END IF;
  IF OLD.max_daily_battles    IS DISTINCT FROM NEW.max_daily_battles    THEN
    v_changes := v_changes || jsonb_build_object('max_daily_battles', NEW.max_daily_battles);
  END IF;
  IF OLD.max_daily_votes      IS DISTINCT FROM NEW.max_daily_votes      THEN
    v_changes := v_changes || jsonb_build_object('max_daily_votes', NEW.max_daily_votes);
  END IF;
  IF OLD.spending_limit_credits IS DISTINCT FROM NEW.spending_limit_credits THEN
    v_changes := v_changes || jsonb_build_object('spending_limit_credits', NEW.spending_limit_credits);
  END IF;

  IF v_changes = '{}' THEN RETURN NEW; END IF;  -- No meaningful change

  PERFORM public.fn_insert_notification(
    v_ai_profile_id,
    'policy_updated',
    'Your agent policy has been updated',
    NULL,
    '/lenser/' || (SELECT handle FROM lensers.profiles WHERE id = v_ai_profile_id) || '/ag/settings',
    jsonb_build_object(
      'ai_lenser_id', NEW.ai_lenser_id,
      'changes',      v_changes
    )
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_policy_updated failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."trg_notify_policy_updated"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."trg_notify_team_run_change"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_ai_profile_id UUID;
  v_owner_id      UUID;
  v_wf_name       TEXT;
  v_notif_type    TEXT;
  v_title         TEXT;
  v_run_url       TEXT;
BEGIN
  -- Determine which transition fired
  IF TG_OP = 'INSERT' AND NEW.status = 'running' THEN
    v_notif_type := 'team_run_started';
    v_title      := 'Run started';
  ELSIF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
    IF   NEW.status = 'completed' THEN
      v_notif_type := 'team_run_completed'; v_title := 'Run completed';
    ELSIF NEW.status = 'failed' THEN
      v_notif_type := 'team_run_failed';    v_title := 'Run failed';
    ELSE
      RETURN NEW;
    END IF;
  ELSE
    RETURN NEW;
  END IF;

  SELECT profile_id INTO v_ai_profile_id
  FROM   agents.ai_lensers WHERE id = NEW.ai_lenser_id;

  SELECT owner_lenser_id INTO v_owner_id
  FROM   agents.ownerships
  WHERE  ai_lenser_id = NEW.ai_lenser_id
  AND    role = 'owner'
  AND    revoked_at IS NULL
  LIMIT  1;

  IF NEW.workflow_id IS NOT NULL THEN
    SELECT title INTO v_wf_name FROM lenses.workflows WHERE id = NEW.workflow_id;
  END IF;

  v_run_url := '/lenser/' ||
    COALESCE((SELECT handle FROM lensers.profiles WHERE id = v_ai_profile_id), 'unknown') ||
    '/ag/runs';

  -- Notify AI lenser
  IF v_ai_profile_id IS NOT NULL THEN
    PERFORM public.fn_insert_notification(
      v_ai_profile_id,
      v_notif_type,
      v_title || COALESCE(': ' || v_wf_name, ''),
      NULL,
      v_run_url,
      jsonb_build_object(
        'team_run_id',   NEW.id,
        'workflow_id',   NEW.workflow_id,
        'workflow_name', v_wf_name,
        'status',        NEW.status
      )
    );
  END IF;

  -- Notify human owner on completion or failure only
  IF v_owner_id IS NOT NULL AND v_notif_type IN ('team_run_completed', 'team_run_failed') THEN
    PERFORM public.fn_insert_notification(
      v_owner_id,
      CASE v_notif_type
        WHEN 'team_run_failed'    THEN 'agent_critical'
        WHEN 'team_run_completed' THEN 'agent_update'
      END,
      'Agent ' || v_title || COALESCE(': ' || v_wf_name, ''),
      NULL,
      v_run_url,
      jsonb_build_object(
        'ai_lenser_id',  NEW.ai_lenser_id,
        'ai_profile_id', v_ai_profile_id,
        'team_run_id',   NEW.id,
        'workflow_name', v_wf_name,
        'status',        NEW.status
      )
    );
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_team_run_change failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."trg_notify_team_run_change"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "agents"."trg_team_runs_capture_agent_snapshot"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
BEGIN
  IF NEW.ai_lenser_id IS NOT NULL AND NEW.redacted_agent_snapshot IS NULL THEN
    NEW.redacted_agent_snapshot := public.fn_redacted_agent_snapshot(NEW.ai_lenser_id);
    NEW.redacted_agent_snapshot_hash := public.fn_redacted_agent_snapshot_hash(NEW.ai_lenser_id);
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "agents"."trg_team_runs_capture_agent_snapshot"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "ai"."cascade_provider_deactivation"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
BEGIN
    IF OLD.is_active = true AND NEW.is_active = false THEN
        UPDATE ai.models
        SET is_active = false
        WHERE provider_id = NEW.id
          AND is_active   = true;

        RAISE NOTICE 'Provider % deactivated: % model(s) set inactive', NEW.key, FOUND;
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "ai"."cascade_provider_deactivation"() OWNER TO "postgres";


COMMENT ON FUNCTION "ai"."cascade_provider_deactivation"() IS 'Trigger function: when a provider is deactivated (is_active → false), automatically sets is_active = false on all of its active models. Runs SECURITY DEFINER so it always has write access to ai.models regardless of invoker.';



CREATE OR REPLACE FUNCTION "ai"."enforce_active_provider_for_model"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
DECLARE
    v_provider_active boolean;
    v_provider_key    text;
BEGIN
    IF NEW.is_active = true THEN
        SELECT p.is_active, p.key
          INTO v_provider_active, v_provider_key
          FROM ai.providers p
         WHERE p.id = NEW.provider_id;

        IF v_provider_active IS NOT TRUE THEN
            RAISE EXCEPTION
                'Cannot activate model "%" — provider "%" (id=%) is inactive or does not exist. '
                'Activate the provider first.',
                NEW.key,
                COALESCE(v_provider_key, 'unknown'),
                NEW.provider_id
            USING ERRCODE = 'P0001';
        END IF;
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "ai"."enforce_active_provider_for_model"() OWNER TO "postgres";


COMMENT ON FUNCTION "ai"."enforce_active_provider_for_model"() IS 'Trigger function: prevents any role from activating a model whose provider is inactive. Raises P0001 with a descriptive message. Applies on BEFORE INSERT OR UPDATE OF is_active.';



CREATE OR REPLACE FUNCTION "ai"."fn_decrypt_api_key"("p_key_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'vault', 'auth', 'public'
    AS $$
DECLARE
    v_encrypted_id uuid;
    v_decrypted    text;
BEGIN
    -- Primary guard: grant restricts to service_role only.
    -- Secondary guard: verify JWT role as defence-in-depth.
    IF auth.role() IS DISTINCT FROM 'service_role' THEN
        RAISE EXCEPTION 'Forbidden: only service_role can decrypt API keys';
    END IF;

    SELECT encrypted_key_id INTO v_encrypted_id
    FROM ai.keys
    WHERE id = p_key_id AND is_active = true;

    IF v_encrypted_id IS NULL THEN
        RAISE EXCEPTION 'Key not found or revoked';
    END IF;

    SELECT decrypted_secret INTO v_decrypted
    FROM vault.decrypted_secrets
    WHERE id = v_encrypted_id;

    IF v_decrypted IS NULL THEN
        RAISE EXCEPTION 'Failed to decrypt key from vault';
    END IF;

    RETURN v_decrypted;
END;
$$;


ALTER FUNCTION "ai"."fn_decrypt_api_key"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "ai"."fn_decrypt_api_key"("p_key_id" "uuid") IS 'Decrypts a BYOK API key from Vault. Restricted to service_role only. Used by the execution backend at runtime. Never exposed to client.';



CREATE OR REPLACE FUNCTION "ai"."fn_get_my_api_keys"() RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "label" "text", "key_suffix" "text", "is_active" boolean, "created_at" timestamp with time zone, "revoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  -- Always resolve to the human profile so this works correctly whether the
  -- caller is in the main workspace or the agent workspace.
  SELECT p.id INTO v_lenser_id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.type = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated: no lenser profile found';
  END IF;

  RETURN QUERY
  SELECT
    k.id,
    k.lenser_id,
    k.provider_id,
    p.key        AS provider_key,
    p.display_name AS provider_name,
    k.label,
    k.key_suffix,
    k.is_active,
    k.created_at,
    k.revoked_at
  FROM ai.keys k
  JOIN ai.providers p ON p.id = k.provider_id
  WHERE k.lenser_id = v_lenser_id
    AND k.is_active = true
  ORDER BY k.created_at DESC;
END;
$$;


ALTER FUNCTION "ai"."fn_get_my_api_keys"() OWNER TO "postgres";


COMMENT ON FUNCTION "ai"."fn_get_my_api_keys"() IS 'Returns active BYOK API keys for the authenticated lenser, joined with provider metadata. SECURITY DEFINER — runs as postgres to bypass table-level restrictions on ai.keys and ai.providers. Decrypted key material is never returned — only the masked suffix. Resolves against the human profile (not get_auth_lenser_id) so it works correctly from the agent workspace.';



CREATE OR REPLACE FUNCTION "ai"."fn_get_my_api_keys_paged"("p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS SETOF "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
        SELECT to_jsonb(t)
        FROM ai.fn_get_my_api_keys() t
        OFFSET GREATEST(coalesce(p_offset, 0), 0)
        LIMIT LEAST(GREATEST(coalesce(p_limit, 50), 1), 100)
      $$;


ALTER FUNCTION "ai"."fn_get_my_api_keys_paged"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "ai"."fn_revoke_api_key"("p_key_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
  v_rows      int;
BEGIN
  -- Always resolve to the human profile so this works correctly whether the
  -- caller is in the main workspace or the agent workspace.
  SELECT p.id INTO v_lenser_id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.type = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated: no lenser profile found';
  END IF;

  UPDATE ai.keys
  SET is_active = false, revoked_at = now()
  WHERE id = p_key_id
    AND lenser_id = v_lenser_id
    AND is_active = true;

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  IF v_rows = 0 THEN
    RAISE EXCEPTION 'Key not found or already revoked';
  END IF;
END;
$$;


ALTER FUNCTION "ai"."fn_revoke_api_key"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "ai"."fn_revoke_api_key"("p_key_id" "uuid") IS 'Soft-revokes a BYOK API key. Only the key owner can revoke. Revoked keys cannot be used for execution. Resolves against the human profile (not get_auth_lenser_id) so it works correctly from the agent workspace.';



CREATE OR REPLACE FUNCTION "ai"."fn_store_api_key"("p_provider" "text", "p_label" "text" DEFAULT NULL::"text", "p_raw_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'vault', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id   uuid;
  v_secret_id   uuid;
  v_suffix      text;
  v_key_id      uuid;
  v_secret_name text;
  v_provider_id uuid;
BEGIN
  -- Always resolve to the human profile so this works correctly whether the
  -- caller is in the main workspace or the agent workspace.
  SELECT p.id INTO v_lenser_id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.type = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated: no lenser profile found';
  END IF;

  IF p_raw_key IS NULL OR length(trim(p_raw_key)) < 8 THEN
    RAISE EXCEPTION 'Invalid API key: must be at least 8 characters';
  END IF;

  SELECT id INTO v_provider_id
  FROM ai.providers
  WHERE key = p_provider AND is_active = true;

  IF v_provider_id IS NULL THEN
    RAISE EXCEPTION 'Unsupported or inactive provider: %', p_provider;
  END IF;

  v_suffix := right(trim(p_raw_key), 4);

  v_secret_name := 'byok_' || v_lenser_id::text || '_' || p_provider || '_' || gen_random_uuid()::text;

  v_secret_id := vault.create_secret(
    trim(p_raw_key),
    v_secret_name,
    'BYOK API key for ' || p_provider
  );

  INSERT INTO ai.keys (lenser_id, provider_id, label, encrypted_key_id, key_suffix)
  VALUES (v_lenser_id, v_provider_id, p_label, v_secret_id, v_suffix)
  RETURNING id INTO v_key_id;

  RETURN v_key_id;
END;
$$;


ALTER FUNCTION "ai"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "ai"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") IS 'Encrypts and stores a BYOK API key in Vault. Multiple keys per provider per lenser are allowed. The vault secret name includes gen_random_uuid() to guarantee uniqueness and avoid hitting the secrets_name_idx unique constraint. Only the last 4 characters are stored in plain text. Returns the key row UUID. Resolves against the human profile (not get_auth_lenser_id) so it works correctly from the agent workspace.';



CREATE OR REPLACE FUNCTION "analytics"."fn_aggregate_cost_daily"("p_date" "date" DEFAULT (CURRENT_DATE - 1)) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'analytics', 'agents', 'lenses', 'ai', 'public'
    AS $$
BEGIN
  INSERT INTO analytics.agent_cost_daily (
    ai_lenser_id,
    model_key,
    provider,
    period_date,
    total_tokens_in,
    total_tokens_out,
    total_credits,
    run_count,
    updated_at
  )
  SELECT
    al.id                                   AS ai_lenser_id,
    COALESCE(kul.model_key, '')             AS model_key,
    COALESCE(kul.provider, '')              AS provider,
    date_trunc('day', kul.created_at)::date AS period_date,
    SUM(COALESCE(kul.token_input, 0))       AS total_tokens_in,
    SUM(COALESCE(kul.token_output, 0))      AS total_tokens_out,
    SUM(COALESCE(kul.credit_cost, 0))       AS total_credits,
    COUNT(kul.id)                           AS run_count,
    now()                                   AS updated_at
  FROM ai.key_usage_log kul
  JOIN agents.ai_lensers al ON al.profile_id = kul.lenser_id
  WHERE date_trunc('day', kul.created_at)::date = p_date
    AND kul.model_key IS NOT NULL
    AND kul.provider  IS NOT NULL
  GROUP BY
    al.id,
    kul.model_key,
    kul.provider,
    date_trunc('day', kul.created_at)::date
  ON CONFLICT (ai_lenser_id, model_key, period_date) DO UPDATE SET
    total_tokens_in  = EXCLUDED.total_tokens_in,
    total_tokens_out = EXCLUDED.total_tokens_out,
    total_credits    = EXCLUDED.total_credits,
    run_count        = EXCLUDED.run_count,
    updated_at       = EXCLUDED.updated_at;
END;
$$;


ALTER FUNCTION "analytics"."fn_aggregate_cost_daily"("p_date" "date") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "analytics"."fn_aggregate_eval_quality_daily"("p_date" "date" DEFAULT (CURRENT_DATE - 1)) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'analytics', 'agents', 'lenses', 'ai', 'public'
    AS $$
BEGIN
  INSERT INTO analytics.eval_quality_daily (
    ai_lenser_id,
    evaluation_id,
    period_date,
    run_count,
    pass_count,
    total_cases,
    passed_cases,
    mean_score
  )
  SELECT
    e.ai_lenser_id                                                    AS ai_lenser_id,
    er.evaluation_id                                                  AS evaluation_id,
    date_trunc('day', er.started_at)::date                           AS period_date,
    COUNT(DISTINCT er.id)                                             AS run_count,
    COUNT(DISTINCT er.id) FILTER (WHERE er.score >= 0.7)             AS pass_count,
    COUNT(ecr.id)                                                     AS total_cases,
    COUNT(ecr.id) FILTER (WHERE ecr.score >= 0.7)                    AS passed_cases,
    AVG(er.score)                                                     AS mean_score
  FROM agents.evaluation_runs er
  JOIN agents.evaluations e
    ON e.id = er.evaluation_id
  LEFT JOIN agents.evaluation_case_results ecr
    ON ecr.evaluation_run_id = er.id
  WHERE date_trunc('day', er.started_at)::date = p_date
    AND e.ai_lenser_id IS NOT NULL
  GROUP BY
    e.ai_lenser_id,
    er.evaluation_id,
    date_trunc('day', er.started_at)::date
  ON CONFLICT (ai_lenser_id, evaluation_id, period_date) DO UPDATE SET
    run_count    = EXCLUDED.run_count,
    pass_count   = EXCLUDED.pass_count,
    total_cases  = EXCLUDED.total_cases,
    passed_cases = EXCLUDED.passed_cases,
    mean_score   = EXCLUDED.mean_score;
END;
$$;


ALTER FUNCTION "analytics"."fn_aggregate_eval_quality_daily"("p_date" "date") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "analytics"."fn_aggregate_workflow_perf_daily"("p_date" "date" DEFAULT (CURRENT_DATE - 1)) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'analytics', 'agents', 'lenses', 'ai', 'public'
    AS $$
BEGIN
  INSERT INTO analytics.workflow_perf_daily (
    ai_lenser_id,
    workflow_id,
    period_date,
    run_count,
    failed_count,
    p50_duration_ms,
    p95_duration_ms
  )
  SELECT
    tr.ai_lenser_id                                                AS ai_lenser_id,
    wr.workflow_id                                                 AS workflow_id,
    date_trunc('day', wr.started_at)::date                       AS period_date,
    COUNT(DISTINCT wr.id)                                          AS run_count,
    COUNT(DISTINCT wr.id) FILTER (WHERE wr.status = 'failed')    AS failed_count,
    percentile_cont(0.50) WITHIN GROUP (
      ORDER BY
        EXTRACT(EPOCH FROM (wr.completed_at - wr.started_at)) * 1000
    )::integer                                                     AS p50_duration_ms,
    percentile_cont(0.95) WITHIN GROUP (
      ORDER BY
        EXTRACT(EPOCH FROM (wr.completed_at - wr.started_at)) * 1000
    )::integer                                                     AS p95_duration_ms
  FROM agents.team_runs tr
  JOIN lenses.workflow_runs wr
    ON wr.id = tr.workflow_run_id
  WHERE date_trunc('day', wr.started_at)::date = p_date
    AND wr.workflow_id   IS NOT NULL
    AND wr.started_at    IS NOT NULL
    AND wr.completed_at  IS NOT NULL
  GROUP BY
    tr.ai_lenser_id,
    wr.workflow_id,
    date_trunc('day', wr.started_at)::date
  ON CONFLICT (ai_lenser_id, workflow_id, period_date) DO UPDATE SET
    run_count       = EXCLUDED.run_count,
    failed_count    = EXCLUDED.failed_count,
    p50_duration_ms = EXCLUDED.p50_duration_ms,
    p95_duration_ms = EXCLUDED.p95_duration_ms;
END;
$$;


ALTER FUNCTION "analytics"."fn_aggregate_workflow_perf_daily"("p_date" "date") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "analytics"."log_tag_view"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid", "p_user_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'analytics', 'content', 'public', 'auth'
    AS $$BEGIN
  INSERT INTO analytics.tag_activity_events (tag_id, entity_type, entity_id, activity_type, actor_id)
  SELECT tag_id, p_entity_type, p_entity_id, 'viewed', p_user_id
  FROM content.tag_map
  WHERE entity_type = p_entity_type AND entity_id = p_entity_id;
END;$$;


ALTER FUNCTION "analytics"."log_tag_view"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid", "p_user_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "analytics"."protect_feedback_system_fields"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'analytics', 'public', 'auth'
    AS $$
BEGIN
  IF TG_OP = 'UPDATE' THEN
    IF NEW.start_date <> OLD.start_date
       OR NEW.end_date <> OLD.end_date
       OR NEW.created_at <> OLD.created_at THEN
         RAISE EXCEPTION 'System-managed timestamps cannot be modified.';
    END IF;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "analytics"."protect_feedback_system_fields"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "analytics"."set_feedback_user_id"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'analytics', 'public', 'auth'
    AS $$
begin
  if auth.uid() is not null then
    new.user_id := auth.uid();
  else
    new.user_id := null;
  end if;

  return new;
end;
$$;


ALTER FUNCTION "analytics"."set_feedback_user_id"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."fn_append_gateway_event"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'devices', 'public', 'extensions'
    AS $$
DECLARE
  v_event_id    UUID;
  v_prev_hash   TEXT;
  v_payload_hex TEXT;
  v_seq         BIGINT;
  v_sub_seq     BIGINT;
  v_canonical   TEXT;
  v_chain_id    UUID;
BEGIN
  IF p_event_type IS NULL OR p_lenser_id IS NULL THEN
    RAISE EXCEPTION 'invalid_input' USING ERRCODE = '22023';
  END IF;

  -- 2a. Insert the underlying audit.events row.
  v_event_id := audit.log_event_v2(
    p_event_type      := p_event_type,
    p_actor_type      := 'lenser',
    p_actor_id        := p_lenser_id,
    p_entity_schema   := CASE WHEN p_device_id IS NOT NULL THEN 'devices' END,
    p_entity_table    := CASE WHEN p_device_id IS NOT NULL THEN 'registered_devices' END,
    p_entity_id       := p_device_id,
    p_payload         := COALESCE(p_payload, '{}'::jsonb),
    p_severity        := 'info'
  );

  -- 2b. Compute prev_hash from the latest gateway-chain row for this lenser.
  SELECT payload_hash, sub_sequence_no
    INTO v_prev_hash, v_sub_seq
  FROM audit.hash_chains
  WHERE chain_kind = 'gateway'
    AND subject_lenser_id = p_lenser_id
  ORDER BY sub_sequence_no DESC NULLS LAST
  LIMIT 1;

  v_sub_seq := COALESCE(v_sub_seq, 0) + 1;

  -- 2c. Canonical payload — JSON object with stable field ordering. We rely on
  --     jsonb_build_object's deterministic key order (alphabetical insertion)
  --     and jsonb_strip_nulls to keep the chain reproducible.
  v_canonical := jsonb_strip_nulls(jsonb_build_object(
    'event_id',      v_event_id,
    'event_type',    p_event_type,
    'lenser_id',     p_lenser_id,
    'device_id',     p_device_id,
    'payload',       COALESCE(p_payload, '{}'::jsonb),
    'sub_seq',       v_sub_seq
  ))::text;

  v_payload_hex := encode(
    extensions.digest(
      COALESCE(v_prev_hash, '') || v_canonical, 'sha256'
    ),
    'hex'
  );

  -- 2d. Allocate global sequence_no (advisory lock keeps it monotonic).
  PERFORM pg_advisory_xact_lock(hashtext('audit.hash_chains.sequence_no')::bigint);
  SELECT COALESCE(MAX(sequence_no), 0) + 1 INTO v_seq FROM audit.hash_chains;

  INSERT INTO audit.hash_chains (
    event_id, prev_hash, payload_hash, sequence_no,
    chain_kind, subject_lenser_id, subject_device_id, sub_sequence_no
  ) VALUES (
    v_event_id, v_prev_hash, v_payload_hex, v_seq,
    'gateway', p_lenser_id, p_device_id, v_sub_seq
  )
  RETURNING id INTO v_chain_id;

  RETURN v_chain_id;
END;
$$;


ALTER FUNCTION "audit"."fn_append_gateway_event"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."fn_append_gateway_event"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") IS 'Append a Trust-Gateway event to audit.events AND chain it on the per-Lenser gateway sub-chain. Used by triggers on devices.registered_devices and execution.attestation_verifications.';



CREATE OR REPLACE FUNCTION "audit"."fn_append_gateway_event_safe"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  BEGIN
    PERFORM audit.fn_append_gateway_event(p_event_type, p_lenser_id, p_device_id, p_payload);
  EXCEPTION WHEN OTHERS THEN
    RAISE NOTICE 'gateway hash chain append failed (%): %', p_event_type, SQLERRM;
  END;
END;
$$;


ALTER FUNCTION "audit"."fn_append_gateway_event_safe"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."fn_assert_webhook_signing_state"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
DECLARE
  v_secret text := current_setting('app.webhook_signing_secret', true);
  v_strict boolean := COALESCE(lower(current_setting('app.webhook_strict_signing', true)) = 'true', false);
  v_blocked int := 0;
BEGIN
  IF v_strict AND (v_secret IS NULL OR length(v_secret) = 0) THEN
    UPDATE audit.webhook_outbox
    SET last_error = 'strict_signing_no_secret',
        next_attempt_at = now() + interval '5 minutes'
    WHERE delivered_at IS NULL
      AND dead_lettered_at IS NULL
      AND next_attempt_at <= now();
    GET DIAGNOSTICS v_blocked = ROW_COUNT;
    RAISE WARNING 'Webhook strict signing is ON but secret is unset. % outbox rows deferred.', v_blocked;
  END IF;

  RETURN jsonb_build_object(
    'strict_mode', v_strict,
    'secret_set', v_secret IS NOT NULL AND length(v_secret) > 0,
    'blocked_rows', v_blocked
  );
END;
$$;


ALTER FUNCTION "audit"."fn_assert_webhook_signing_state"() OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."fn_assert_webhook_signing_state"() IS 'Phase P. Called at the top of fn_dispatch_webhook_outbox. When strict signing is ON and the secret is unset, defers all eligible rows by 5 minutes and tags them with last_error=strict_signing_no_secret. Returns a jsonb summary.';



CREATE OR REPLACE FUNCTION "audit"."fn_chain_verify"("p_lenser_id" "uuid", "p_chain_kind" "text" DEFAULT 'gateway'::"text") RETURNS TABLE("ok" boolean, "total_rows" bigint, "first_break_id" "uuid", "first_break_seq" bigint, "reason" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_row             audit.hash_chains%ROWTYPE;
  v_event           audit.events%ROWTYPE;
  v_prev_hash       TEXT;
  v_canonical       TEXT;
  v_expected_hash   TEXT;
  v_total           BIGINT := 0;
  v_break_id        UUID;
  v_break_seq       BIGINT;
  v_break_reason    TEXT;
  v_caller_role     TEXT := COALESCE(auth.role(), current_user);
  v_caller_owns     BOOLEAN := false;
BEGIN
  IF p_chain_kind NOT IN ('gateway') THEN
    RAISE EXCEPTION 'unsupported_chain_kind';
  END IF;

  IF p_lenser_id IS NULL THEN
    RAISE EXCEPTION 'lenser_required' USING ERRCODE = '22023';
  END IF;

  IF v_caller_role <> 'service_role' THEN
    SELECT EXISTS (
      SELECT 1
      FROM lensers.profiles lp
      WHERE lp.id = p_lenser_id
        AND lp.user_id = auth.uid()
    ) INTO v_caller_owns;

    IF NOT COALESCE(v_caller_owns, false) THEN
      RAISE EXCEPTION 'chain_verify_owner_required' USING ERRCODE = '42501';
    END IF;
  END IF;

  v_prev_hash := NULL;
  FOR v_row IN
    SELECT *
    FROM audit.hash_chains
    WHERE chain_kind = p_chain_kind
      AND subject_lenser_id = p_lenser_id
    ORDER BY sub_sequence_no ASC
  LOOP
    v_total := v_total + 1;
    SELECT * INTO v_event FROM audit.events WHERE id = v_row.event_id;
    IF v_event.id IS NULL THEN
      v_break_id := v_row.id;
      v_break_seq := v_row.sub_sequence_no;
      v_break_reason := 'event_missing';
      EXIT;
    END IF;

    v_canonical := jsonb_strip_nulls(jsonb_build_object(
      'event_id',   v_event.id,
      'event_type', v_event.event_type,
      'lenser_id',  v_row.subject_lenser_id,
      'device_id',  v_row.subject_device_id,
      'payload',    v_event.payload,
      'sub_seq',    v_row.sub_sequence_no
    ))::text;

    v_expected_hash := encode(
      extensions.digest(COALESCE(v_prev_hash, '') || v_canonical, 'sha256'),
      'hex'
    );

    IF v_expected_hash IS DISTINCT FROM v_row.payload_hash THEN
      v_break_id := v_row.id;
      v_break_seq := v_row.sub_sequence_no;
      v_break_reason := 'payload_hash_mismatch';
      EXIT;
    END IF;
    IF COALESCE(v_row.prev_hash, '') IS DISTINCT FROM COALESCE(v_prev_hash, '') THEN
      v_break_id := v_row.id;
      v_break_seq := v_row.sub_sequence_no;
      v_break_reason := 'prev_hash_mismatch';
      EXIT;
    END IF;

    v_prev_hash := v_row.payload_hash;
  END LOOP;

  IF v_break_id IS NULL THEN
    RETURN QUERY SELECT true, v_total, NULL::UUID, NULL::BIGINT, NULL::TEXT;
  ELSE
    RETURN QUERY SELECT false, v_total, v_break_id, v_break_seq, v_break_reason;
  END IF;
END;
$$;


ALTER FUNCTION "audit"."fn_chain_verify"("p_lenser_id" "uuid", "p_chain_kind" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."fn_chain_verify"("p_lenser_id" "uuid", "p_chain_kind" "text") IS 'Walk a caller-owned gateway hash chain and verify every link. service_role may verify any chain; authenticated users may verify only their own Lenser chain.';



CREATE OR REPLACE FUNCTION "audit"."fn_cron_dispatch_webhook_outbox"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
DECLARE
  v_state jsonb;
  v_dispatched int;
BEGIN
  v_state := audit.fn_assert_webhook_signing_state();
  IF (v_state->>'strict_mode')::boolean AND NOT (v_state->>'secret_set')::boolean THEN
    RETURN 0;
  END IF;
  SELECT audit.fn_dispatch_webhook_outbox(50) INTO v_dispatched;
  RETURN v_dispatched;
END;
$$;


ALTER FUNCTION "audit"."fn_cron_dispatch_webhook_outbox"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."fn_dispatch_webhook_outbox"("p_batch" integer DEFAULT 50) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'extensions', 'public'
    AS $$
DECLARE
  r              record;
  v_secret       text;
  v_signature    text;
  v_body_text    text;
  v_processed    int := 0;
  v_new_attempts int;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net') THEN
    RAISE NOTICE 'fn_dispatch_webhook_outbox: pg_net not installed; skipping.';
    RETURN 0;
  END IF;

  v_secret := NULLIF(current_setting('app.webhook_signing_secret', true), '');

  FOR r IN
    SELECT id, event_type, payload, target_url, attempts, max_attempts
    FROM   audit.webhook_outbox
    WHERE  delivered_at      IS NULL
      AND  dead_lettered_at  IS NULL
      AND  next_attempt_at   <= now()
    ORDER BY next_attempt_at ASC
    LIMIT  p_batch
    FOR UPDATE SKIP LOCKED
  LOOP
    v_processed := v_processed + 1;

    BEGIN
      v_body_text := r.payload::text;

      IF v_secret IS NULL THEN
        -- No secret configured — emit a placeholder so receivers can detect
        -- mis-configuration rather than silently accept unsigned traffic.
        v_signature := 'unsigned';
      ELSE
        v_signature :=
          'sha256=' ||
          encode(extensions.hmac(v_body_text, v_secret, 'sha256'), 'hex');
      END IF;

      PERFORM net.http_post(
        url     := r.target_url,
        headers := jsonb_build_object(
          'Content-Type',             'application/json',
          'X-Lenserfight-Webhook',    r.event_type,
          'X-Lenserfight-Version',    '1',
          'X-Lenserfight-Signature',  v_signature
        ),
        body    := r.payload,
        timeout_milliseconds := 5000
      );

      UPDATE audit.webhook_outbox
      SET    delivered_at = now(),
             last_error   = NULL
      WHERE  id = r.id;

    EXCEPTION WHEN OTHERS THEN
      v_new_attempts := r.attempts + 1;

      UPDATE audit.webhook_outbox
      SET    attempts        = v_new_attempts,
             last_error      = left(SQLERRM, 1000),
             next_attempt_at = now() + (interval '1 minute' * power(2, v_new_attempts)::int),
             dead_lettered_at = CASE
               WHEN v_new_attempts >= r.max_attempts THEN now()
               ELSE dead_lettered_at
             END
      WHERE  id = r.id;
    END;
  END LOOP;

  RETURN v_processed;
END;
$$;


ALTER FUNCTION "audit"."fn_dispatch_webhook_outbox"("p_batch" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."fn_dispatch_webhook_outbox"("p_batch" integer) IS 'Phase P3: drains audit.webhook_outbox in batches. Signs body with HMAC-SHA256 using app.webhook_signing_secret (header X-Lenserfight-Signature: sha256=<hex>). On exception increments attempts, schedules exponential backoff (2^attempts minutes), and dead-letters at attempts >= max_attempts. Returns rows processed in the batch.';



CREATE OR REPLACE FUNCTION "audit"."fn_notify_moderation_flagged"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
DECLARE
  v_url     text;
  v_payload jsonb;
BEGIN
  IF NEW.decision_type IS DISTINCT FROM 'flagged' THEN
    RETURN NEW;
  END IF;

  v_url := NULLIF(current_setting('app.moderation_webhook_url', true), '');
  IF v_url IS NULL THEN
    RETURN NEW;
  END IF;

  v_payload := jsonb_build_object(
    'webhook_version',       1,
    'event',                 'moderation_flagged',
    'decision_id',           NEW.id,
    'target_entity_schema',  NEW.target_entity_schema,
    'target_entity_table',   NEW.target_entity_table,
    'target_entity_id',      NEW.target_entity_id,
    'decision_type',         NEW.decision_type,
    'reason',                NEW.reason,
    'is_ai_moderated',       NEW.is_ai_moderated,
    'ai_confidence',         NEW.ai_confidence,
    'moderator_lenser_id',   NEW.moderator_lenser_id,
    'occurred_at',           to_char(NEW.occurred_at AT TIME ZONE 'UTC',
                                     'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
    'battle_id',             CASE
                               WHEN NEW.target_entity_schema = 'battles'
                                AND NEW.target_entity_table  = 'battles'
                               THEN NEW.target_entity_id
                               ELSE NULL
                             END,
    'submission_id',         CASE
                               WHEN NEW.target_entity_schema = 'battles'
                                AND NEW.target_entity_table  IN ('contenders', 'submissions')
                               THEN NEW.target_entity_id
                               ELSE NULL
                             END
  );

  INSERT INTO audit.webhook_outbox (event_type, payload, target_url)
  VALUES ('moderation_flagged', v_payload, v_url);

  RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."fn_notify_moderation_flagged"() OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."fn_notify_moderation_flagged"() IS 'Phase P3: AFTER INSERT trigger on audit.moderation_decisions. Enqueues a moderation_flagged webhook row in audit.webhook_outbox when decision_type=flagged and app.moderation_webhook_url is set. Delivery is handled by audit.fn_dispatch_webhook_outbox.';



CREATE OR REPLACE FUNCTION "audit"."log_event"("p_event_type" "text", "p_entity_schema" "text" DEFAULT NULL::"text", "p_entity_table" "text" DEFAULT NULL::"text", "p_entity_id" "uuid" DEFAULT NULL::"uuid", "p_actor_lenser_id" "uuid" DEFAULT NULL::"uuid", "p_actor_actor_id" "uuid" DEFAULT NULL::"uuid", "p_payload" "jsonb" DEFAULT '{}'::"jsonb", "p_severity" "text" DEFAULT 'info'::"text", "p_ip_hash" "text" DEFAULT NULL::"text", "p_user_agent_hash" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit'
    AS $$
DECLARE
    v_id uuid;
BEGIN
    INSERT INTO "audit"."events" (
        event_type, entity_schema, entity_table, entity_id,
        actor_lenser_id, actor_actor_id, payload, severity,
        ip_hash, user_agent_hash
    ) VALUES (
        p_event_type, p_entity_schema, p_entity_table, p_entity_id,
        p_actor_lenser_id, p_actor_actor_id, p_payload, p_severity,
        p_ip_hash, p_user_agent_hash
    ) RETURNING id INTO v_id;
    RETURN v_id;
END;
$$;


ALTER FUNCTION "audit"."log_event"("p_event_type" "text", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_actor_lenser_id" "uuid", "p_actor_actor_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."log_event"("p_event_type" "text", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_actor_lenser_id" "uuid", "p_actor_actor_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") IS 'DEPRECATED (2026-03-25): Use audit.log_event_v2 instead. This 10-parameter overload uses a split actor model (actor_lenser_id + actor_actor_id) that has been superseded by the unified actor model in log_event_v2 (actor_type + actor_id). Will be removed after all call sites migrate. Do not add new call sites.';



CREATE OR REPLACE FUNCTION "audit"."log_event_v2"("p_event_type" "text", "p_actor_type" "text" DEFAULT 'system'::"text", "p_actor_id" "uuid" DEFAULT NULL::"uuid", "p_entity_schema" "text" DEFAULT NULL::"text", "p_entity_table" "text" DEFAULT NULL::"text", "p_entity_id" "uuid" DEFAULT NULL::"uuid", "p_payload" "jsonb" DEFAULT '{}'::"jsonb", "p_severity" "text" DEFAULT 'info'::"text", "p_ip_hash" "text" DEFAULT NULL::"text", "p_user_agent_hash" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
DECLARE
    v_id "uuid";
BEGIN
    IF "p_actor_type" NOT IN ('lenser', 'ai_actor', 'system') THEN
        RAISE EXCEPTION 'Invalid actor_type: %. Must be lenser, ai_actor, or system.', "p_actor_type";
    END IF;

    INSERT INTO "audit"."events" (
        "event_type", "actor_type", "actor_id",
        "entity_schema", "entity_table", "entity_id",
        "payload", "severity", "ip_hash", "user_agent_hash",
        -- also populate old split columns for backward compat
        "actor_lenser_id", "actor_actor_id"
    )
    VALUES (
        "p_event_type", "p_actor_type", "p_actor_id",
        "p_entity_schema", "p_entity_table", "p_entity_id",
        "p_payload", "p_severity", "p_ip_hash", "p_user_agent_hash",
        CASE WHEN "p_actor_type" = 'lenser'   THEN "p_actor_id" ELSE NULL END,
        CASE WHEN "p_actor_type" = 'ai_actor' THEN "p_actor_id" ELSE NULL END
    )
    RETURNING "id" INTO v_id;

    RETURN v_id;
END;
$$;


ALTER FUNCTION "audit"."log_event_v2"("p_event_type" "text", "p_actor_type" "text", "p_actor_id" "uuid", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "audit"."log_event_v2"("p_event_type" "text", "p_actor_type" "text", "p_actor_id" "uuid", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") IS 'Preferred audit event insertion. Uses unified actor_type + actor_id. Replaces audit.log_event() which uses split actor_lenser_id/actor_actor_id columns. SECURITY DEFINER — call from triggers or Edge Functions.';



CREATE OR REPLACE FUNCTION "audit"."trg_artifact_visibility_changed_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'execution', 'public'
    AS $$
DECLARE
    v_requester_id uuid;
BEGIN
    IF OLD."visibility" = NEW."visibility" THEN
        RETURN NEW;
    END IF;

    -- Resolve the requester through run → request chain
    SELECT req."requester_lenser_id" INTO v_requester_id
    FROM "execution"."runs" r
    JOIN "execution"."requests" req ON req."id" = r."request_id"
    WHERE r."id" = NEW."run_id";

    INSERT INTO "audit"."events" (
        "event_type",
        "entity_schema",
        "entity_table",
        "entity_id",
        "actor_type",
        "actor_id",
        "payload"
    ) VALUES (
        'artifact.visibility_changed',
        'execution',
        'artifacts',
        NEW."id",
        'lenser',
        v_requester_id,
        jsonb_build_object(
            'run_id', NEW."run_id",
            'old_visibility', OLD."visibility",
            'new_visibility', NEW."visibility"
        )
    );

    RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_artifact_visibility_changed_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_backfill_unified_actor"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
  -- If unified columns are already set, nothing to do
  IF NEW.actor_type IS NOT NULL AND NEW.actor_id IS NOT NULL THEN
    RETURN NEW;
  END IF;

  -- Backfill from legacy columns
  IF NEW.actor_type IS NULL AND NEW.actor_lenser_id IS NOT NULL THEN
    NEW.actor_type := 'lenser';
    NEW.actor_id   := NEW.actor_lenser_id;
  ELSIF NEW.actor_type IS NULL AND NEW.actor_actor_id IS NOT NULL THEN
    NEW.actor_type := 'ai_actor';
    NEW.actor_id   := NEW.actor_actor_id;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_backfill_unified_actor"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_battle_status_audit"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'battles', 'lensers'
    AS $$
DECLARE
    v_event_type text;
BEGIN
    -- Map status transitions to event types
    v_event_type := CASE NEW.status
        WHEN 'open'      THEN 'battle.published'
        WHEN 'voting'    THEN 'battle.voting_opened'
        WHEN 'scoring'   THEN 'battle.scoring_started'
        WHEN 'closed'    THEN 'battle.finalized'
        WHEN 'archived'  THEN 'battle.closed'
        ELSE 'battle.status_changed'
    END;

    IF OLD.status IS DISTINCT FROM NEW.status THEN
        PERFORM audit.log_event(
            v_event_type,
            'battles',
            'battles',
            NEW.id,
            NEW.creator_lenser_id,
            NULL,
            jsonb_build_object(
                'old_status', OLD.status,
                'new_status', NEW.status,
                'title',      NEW.title,
                'slug',       NEW.slug
            ),
            'info'
        );
    END IF;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_battle_status_audit"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_execution_run_created_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'execution', 'public'
    AS $$
DECLARE
    v_requester_id uuid;
BEGIN
    -- Resolve the requester from the linked request
    SELECT "requester_lenser_id" INTO v_requester_id
    FROM "execution"."requests"
    WHERE "id" = NEW."request_id";

    INSERT INTO "audit"."events" (
        "event_type",
        "entity_schema",
        "entity_table",
        "entity_id",
        "actor_type",
        "actor_id",
        "payload"
    ) VALUES (
        'execution.created',
        'execution',
        'runs',
        NEW."id",
        'lenser',
        v_requester_id,
        jsonb_build_object(
            'request_id', NEW."request_id",
            'status', NEW."status",
            'billing_status', NEW."billing_status"
        )
    );

    RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_execution_run_created_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_fn_audit_actor"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'actors'
    AS $$
BEGIN
    IF OLD.status IS DISTINCT FROM NEW.status THEN
        PERFORM "audit"."log_event"(
            'actor_status_changed',
            'actors', 'actors', NEW.id,
            NULL, NEW.id,
            jsonb_build_object('old_status', OLD.status::text, 'new_status', NEW.status::text),
            CASE WHEN NEW.status::text = 'suspended' THEN 'warn' ELSE 'info' END
        );
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_fn_audit_actor"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_fn_audit_lenser_profile"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'lensers'
    AS $$
BEGIN
    IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
        PERFORM "audit"."log_event"(
            'lenser_profile_status_changed',
            'lensers', 'profiles', NEW.id,
            NEW.id, NULL,
            jsonb_build_object('old_status', OLD.status, 'new_status', NEW.status),
            CASE WHEN NEW.status IN ('suspended','deleted') THEN 'warn' ELSE 'info' END
        );
    END IF;

    IF TG_OP = 'DELETE' THEN
        PERFORM "audit"."log_event"(
            'lenser_profile_deleted',
            'lensers', 'profiles', OLD.id,
            OLD.id, NULL,
            jsonb_build_object('deleted_status', OLD.status),
            'warn'
        );
    END IF;

    RETURN COALESCE(NEW, OLD);
END;
$$;


ALTER FUNCTION "audit"."trg_fn_audit_lenser_profile"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_fn_audit_wallet_account"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'wallet'
    AS $$
BEGIN
    IF OLD.frozen IS DISTINCT FROM NEW.frozen THEN
        PERFORM "audit"."log_event"(
            CASE WHEN NEW.frozen THEN 'wallet_account_frozen' ELSE 'wallet_account_unfrozen' END,
            'wallet', 'accounts', NEW.id,
            NEW.lenser_id, NULL,
            jsonb_build_object('frozen', NEW.frozen),
            CASE WHEN NEW.frozen THEN 'warn' ELSE 'info' END
        );
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_fn_audit_wallet_account"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_prevent_delete"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'audit'
    AS $$
BEGIN
    RAISE EXCEPTION 'Audit records are immutable. Deletion is not permitted. Table: %', TG_TABLE_NAME;
END;
$$;


ALTER FUNCTION "audit"."trg_prevent_delete"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "audit"."trg_workflow_run_lifecycle_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  -- Log on INSERT (run creation)
  IF TG_OP = 'INSERT' THEN
    INSERT INTO audit.events (
      event_type,
      entity_schema,
      entity_table,
      entity_id,
      actor_lenser_id,
      payload
    ) VALUES (
      'workflow_run_created',
      'lenses',
      'workflow_runs',
      NEW.id,
      NEW.triggered_by,
      jsonb_build_object(
        'workflow_id',     NEW.workflow_id,
        'status',          NEW.status,
        'global_model_id', NEW.global_model_id,
        'budget_credits',  NEW.budget_credits
      )
    );
  END IF;

  -- Log on status change
  IF TG_OP = 'UPDATE' AND OLD.status <> NEW.status THEN
    INSERT INTO audit.events (
      event_type,
      entity_schema,
      entity_table,
      entity_id,
      actor_lenser_id,
      payload
    ) VALUES (
      'workflow_run_' || NEW.status,
      'lenses',
      'workflow_runs',
      NEW.id,
      NEW.triggered_by,
      jsonb_build_object(
        'workflow_id',     NEW.workflow_id,
        'old_status',      OLD.status,
        'new_status',      NEW.status,
        'global_model_id', NEW.global_model_id,
        'budget_credits',  NEW.budget_credits,
        'spent_credits',   NEW.spent_credits
      )
    );
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "audit"."trg_workflow_run_lifecycle_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_approve_device_request"("p_user_code" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'lensers', 'auth', 'public'
    AS $$
DECLARE
  v_user_id uuid := auth.uid();
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_request authz.device_approval_requests%ROWTYPE;
  v_normalized_code text := upper(replace(trim(COALESCE(p_user_code, '')), '-', ''));
BEGIN
  IF v_user_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT *
  INTO v_request
  FROM authz.device_approval_requests
  WHERE replace(user_code, '-', '') = v_normalized_code
  ORDER BY created_at DESC
  LIMIT 1
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN jsonb_build_object(
      'requestId', NULL,
      'status', 'not_found',
      'approvedAt', NULL,
      'expiresAt', NULL
    );
  END IF;

  IF now() > v_request.expires_at THEN
    UPDATE authz.device_approval_requests
    SET status = 'expired'
    WHERE id = v_request.id
      AND status <> 'exchanged';

    RETURN jsonb_build_object(
      'requestId', v_request.id,
      'status', 'expired',
      'approvedAt', v_request.approved_at,
      'expiresAt', v_request.expires_at,
      'label', v_request.label
    );
  END IF;

  IF v_request.status IN ('approved', 'exchanged') THEN
    RETURN jsonb_build_object(
      'requestId', v_request.id,
      'status', 'approved',
      'approvedAt', v_request.approved_at,
      'expiresAt', v_request.expires_at,
      'label', v_request.label
    );
  END IF;

  UPDATE authz.device_approval_requests
  SET status = 'approved',
      approved_at = COALESCE(approved_at, now()),
      approved_by_user_id = COALESCE(approved_by_user_id, v_user_id),
      approved_by_lenser_id = COALESCE(approved_by_lenser_id, v_lenser_id)
  WHERE id = v_request.id;

  RETURN jsonb_build_object(
    'requestId', v_request.id,
    'status', 'approved',
    'approvedAt', COALESCE(v_request.approved_at, now()),
    'expiresAt', v_request.expires_at,
    'label', v_request.label
  );
END;
$$;


ALTER FUNCTION "authz"."fn_approve_device_request"("p_user_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'lensers', 'auth', 'extensions', 'public'
    AS $$
DECLARE
  v_request authz.device_approval_requests%ROWTYPE;
  v_expected_secret_hash text;
  v_token_plain text;
  v_token_hash text;
  v_token_id uuid;
  v_token_prefix text;
  v_token_created_at timestamptz;
  v_token_expires_at timestamptz;
  v_poll_interval_seconds integer := 5;
BEGIN
  IF auth.uid() IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT *
  INTO v_request
  FROM authz.device_approval_requests
  WHERE id = p_request_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN jsonb_build_object(
      'requestId', p_request_id,
      'status', 'invalid',
      'pollIntervalSeconds', v_poll_interval_seconds,
      'expiresAt', NULL
    );
  END IF;

  v_expected_secret_hash := encode(digest(COALESCE(p_request_secret, ''), 'sha256'), 'hex');

  IF v_expected_secret_hash IS DISTINCT FROM v_request.request_secret_hash THEN
    RAISE EXCEPTION 'Invalid device approval request';
  END IF;

  IF now() > v_request.expires_at THEN
    UPDATE authz.device_approval_requests
    SET status = 'expired'
    WHERE id = v_request.id
      AND status <> 'exchanged';

    RETURN jsonb_build_object(
      'requestId', v_request.id,
      'status', 'expired',
      'pollIntervalSeconds', v_poll_interval_seconds,
      'expiresAt', v_request.expires_at
    );
  END IF;

  IF v_request.status = 'pending' THEN
    RETURN jsonb_build_object(
      'requestId', v_request.id,
      'status', 'pending',
      'pollIntervalSeconds', v_poll_interval_seconds,
      'expiresAt', v_request.expires_at,
      'approvedAt', v_request.approved_at,
      'label', v_request.label
    );
  END IF;

  v_token_plain := encode(digest(v_request.id::text || ':' || p_request_secret, 'sha256'), 'hex');
  v_token_hash := encode(digest(v_token_plain, 'sha256'), 'hex');
  v_token_prefix := left(v_token_plain, 8);
  v_token_expires_at := now() + make_interval(hours => v_request.requested_token_ttl_hours);

  SELECT id, created_at
  INTO v_token_id, v_token_created_at
  FROM authz.developer_tokens
  WHERE issued_from_request_id = v_request.id
  LIMIT 1;

  IF v_token_id IS NULL THEN
    INSERT INTO authz.developer_tokens (
        lenser_id,
        label,
        token_hash,
        token_prefix,
        issued_from_request_id,
        expires_at
    ) VALUES (
        COALESCE(v_request.approved_by_lenser_id, v_request.requested_by_lenser_id, lensers.get_auth_lenser_id()),
        v_request.label,
        v_token_hash,
        v_token_prefix,
        v_request.id,
        v_token_expires_at
    )
    RETURNING id, created_at INTO v_token_id, v_token_created_at;

    UPDATE authz.device_approval_requests
    SET status = 'exchanged',
        exchanged_at = now(),
        developer_token_id = v_token_id
    WHERE id = v_request.id;
  ELSE
    UPDATE authz.device_approval_requests
    SET status = 'exchanged',
        exchanged_at = COALESCE(exchanged_at, now()),
        developer_token_id = COALESCE(developer_token_id, v_token_id)
    WHERE id = v_request.id;
  END IF;

  RETURN jsonb_build_object(
    'requestId', v_request.id,
    'status', 'approved',
    'pollIntervalSeconds', v_poll_interval_seconds,
    'expiresAt', v_token_expires_at,
    'approvedAt', v_request.approved_at,
    'tokenId', v_token_id,
    'token', v_token_plain,
    'label', v_request.label,
    'tokenPrefix', v_token_prefix,
    'createdAt', v_token_created_at
  );
END;
$$;


ALTER FUNCTION "authz"."fn_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'extensions', 'public'
    AS $$
DECLARE
  v_request        authz.device_approval_requests%ROWTYPE;
  v_expected_hash  text;
  v_poll_interval  integer := 5;
BEGIN
  SELECT *
  INTO v_request
  FROM authz.device_approval_requests
  WHERE id = p_request_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN jsonb_build_object(
      'status',              'invalid',
      'pollIntervalSeconds', v_poll_interval
    );
  END IF;

  v_expected_hash := encode(digest(COALESCE(p_request_secret, ''), 'sha256'), 'hex');
  IF v_expected_hash IS DISTINCT FROM v_request.request_secret_hash THEN
    RAISE EXCEPTION 'Invalid device login request';
  END IF;

  IF v_request.status = 'exchanged' THEN
    RETURN jsonb_build_object(
      'status',              'invalid',
      'pollIntervalSeconds', v_poll_interval
    );
  END IF;

  IF now() > v_request.expires_at THEN
    UPDATE authz.device_approval_requests
    SET status = 'expired'
    WHERE id = v_request.id
      AND status <> 'exchanged';

    RETURN jsonb_build_object(
      'status',              'expired',
      'pollIntervalSeconds', v_poll_interval,
      'expiresAt',           v_request.expires_at
    );
  END IF;

  IF v_request.status = 'pending' OR v_request.login_access_token IS NULL THEN
    RETURN jsonb_build_object(
      'status',              'pending',
      'pollIntervalSeconds', v_poll_interval,
      'expiresAt',           v_request.expires_at
    );
  END IF;

  UPDATE authz.device_approval_requests
  SET status              = 'exchanged',
      exchanged_at        = now(),
      login_access_token  = NULL,
      login_refresh_token = NULL
  WHERE id = v_request.id;

  RETURN jsonb_build_object(
    'status',              'approved',
    'accessToken',         v_request.login_access_token,
    'refreshToken',        v_request.login_refresh_token,
    'pollIntervalSeconds', v_poll_interval
  );
END;
$$;


ALTER FUNCTION "authz"."fn_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_generate_user_code"() RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'extensions', 'public'
    AS $$
DECLARE
  v_code text;
BEGIN
  v_code := upper(substr(md5(gen_random_uuid()::text), 1, 8));
  RETURN substr(v_code, 1, 4) || '-' || substr(v_code, 5, 4);
END;
$$;


ALTER FUNCTION "authz"."fn_generate_user_code"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_list_developer_tokens"() RETURNS TABLE("id" "uuid", "label" "text", "tokenPrefix" "text", "status" "text", "expiresAt" timestamp with time zone, "createdAt" timestamp with time zone, "revokedAt" timestamp with time zone, "lastUsedAt" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'lensers', 'auth', 'public'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  RETURN QUERY
  SELECT
    t.id,
    t.label,
    t.token_prefix AS "tokenPrefix",
    CASE
      WHEN t.revoked_at IS NOT NULL THEN 'revoked'
      WHEN t.expires_at <= now() THEN 'expired'
      ELSE 'active'
    END AS status,
    t.expires_at AS "expiresAt",
    t.created_at AS "createdAt",
    t.revoked_at AS "revokedAt",
    t.last_used_at AS "lastUsedAt"
  FROM authz.developer_tokens t
  WHERE t.lenser_id = v_lenser_id
  ORDER BY t.created_at DESC;
END;
$$;


ALTER FUNCTION "authz"."fn_list_developer_tokens"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_list_developer_tokens_paged"("p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS SETOF "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT to_jsonb(t)
  FROM authz.fn_list_developer_tokens() t
  OFFSET GREATEST(COALESCE(p_offset, 0), 0)
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 100)
$$;


ALTER FUNCTION "authz"."fn_list_developer_tokens_paged"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_request_device_approval"("p_label" "text" DEFAULT NULL::"text", "p_request_ttl_minutes" integer DEFAULT 10, "p_token_ttl_hours" integer DEFAULT 24) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'lensers', 'auth', 'extensions', 'public'
    AS $$
DECLARE
  v_request_id uuid := gen_random_uuid();
  v_request_secret text := encode(gen_random_bytes(32), 'hex');
  v_request_secret_hash text := encode(digest(v_request_secret, 'sha256'), 'hex');
  v_user_code text := authz.fn_generate_user_code();
  v_requested_by_user_id uuid := auth.uid();
  v_requested_by_lenser_id uuid := lensers.get_auth_lenser_id();
  v_request_ttl_minutes integer := LEAST(GREATEST(COALESCE(p_request_ttl_minutes, 10), 1), 10);
  v_token_ttl_hours integer := LEAST(GREATEST(COALESCE(p_token_ttl_hours, 24), 1), 24);
  v_expires_at timestamptz := now() + make_interval(mins => LEAST(GREATEST(COALESCE(p_request_ttl_minutes, 10), 1), 10));
BEGIN
  IF v_requested_by_user_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  INSERT INTO authz.device_approval_requests (
      id,
      user_code,
      request_secret_hash,
      label,
      requested_by_user_id,
      requested_by_lenser_id,
      requested_token_ttl_hours,
      expires_at
  ) VALUES (
      v_request_id,
      v_user_code,
      v_request_secret_hash,
      p_label,
      v_requested_by_user_id,
      v_requested_by_lenser_id,
      v_token_ttl_hours,
      v_expires_at
  );

  RETURN jsonb_build_object(
    'requestId', v_request_id,
    'requestSecret', v_request_secret,
    'userCode', v_user_code,
    'verificationUri', '/device-approval?code=' || v_user_code,
    'verificationUriComplete', '/device-approval?code=' || v_user_code || '&request_id=' || v_request_id::text,
    'pollIntervalSeconds', 5,
    'expiresAt', v_expires_at,
    'status', 'pending',
    'label', p_label
  );
END;
$$;


ALTER FUNCTION "authz"."fn_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_request_device_login"("p_request_ttl_minutes" integer DEFAULT 10) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'extensions', 'public'
    AS $$
DECLARE
  v_request_id          uuid        := gen_random_uuid();
  v_request_secret      text        := encode(gen_random_bytes(32), 'hex');
  v_request_secret_hash text        := encode(digest(v_request_secret, 'sha256'), 'hex');
  v_user_code           text        := authz.fn_generate_user_code();
  v_ttl                 integer     := LEAST(GREATEST(COALESCE(p_request_ttl_minutes, 10), 1), 10);
  v_expires_at          timestamptz := now() + make_interval(mins => v_ttl);
BEGIN
  -- No auth.uid() check — this is the unauthenticated login initiation path.
  INSERT INTO authz.device_approval_requests (
      id,
      user_code,
      request_secret_hash,
      label,
      requested_by_user_id,
      requested_by_lenser_id,
      requested_token_ttl_hours,
      expires_at
  ) VALUES (
      v_request_id,
      v_user_code,
      v_request_secret_hash,
      'CLI Login',
      NULL,
      NULL,
      0,
      v_expires_at
  );

  RETURN jsonb_build_object(
    'requestId',           v_request_id,
    'requestSecret',       v_request_secret,
    'userCode',            v_user_code,
    'verificationUri',     '/device-approval?code=' || v_user_code || '&mode=login',
    'pollIntervalSeconds', 5,
    'expiresAt',           v_expires_at,
    'status',              'pending'
  );
END;
$$;


ALTER FUNCTION "authz"."fn_request_device_login"("p_request_ttl_minutes" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_revoke_developer_token"("p_token_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'lensers', 'auth', 'public'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_rows integer;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE authz.developer_tokens
  SET revoked_at = COALESCE(revoked_at, now()),
      status = 'revoked'
  WHERE id = p_token_id
    AND lenser_id = v_lenser_id
    AND revoked_at IS NULL;

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  IF v_rows = 0 THEN
    RAISE EXCEPTION 'Developer token not found or already revoked';
  END IF;
END;
$$;


ALTER FUNCTION "authz"."fn_revoke_developer_token"("p_token_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "authz"."fn_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'authz', 'auth', 'public'
    AS $$
DECLARE
  v_user_id        uuid := auth.uid();
  v_normalized     text := upper(replace(trim(COALESCE(p_user_code, '')), '-', ''));
  v_request        authz.device_approval_requests%ROWTYPE;
BEGIN
  IF v_user_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT *
  INTO v_request
  FROM authz.device_approval_requests
  WHERE replace(user_code, '-', '') = v_normalized
    AND status IN ('pending', 'approved')
    AND expires_at > now()
  ORDER BY created_at DESC
  LIMIT 1
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Device login request not found or expired';
  END IF;

  UPDATE authz.device_approval_requests
  SET status               = 'approved',
      approved_at          = COALESCE(approved_at, now()),
      approved_by_user_id  = COALESCE(approved_by_user_id, v_user_id),
      login_access_token   = p_access_token,
      login_refresh_token  = p_refresh_token
  WHERE id = v_request.id;

  RETURN jsonb_build_object('status', 'stored');
END;
$$;


ALTER FUNCTION "authz"."fn_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "automation"."fn_check_filter_keys"("p_filter" "jsonb") RETURNS boolean
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'pg_catalog'
    AS $_$
DECLARE
  v_key text;
BEGIN
  IF p_filter IS NULL OR jsonb_typeof(p_filter) <> 'object' THEN
    RETURN p_filter IS NULL OR p_filter = '{}'::jsonb;
  END IF;

  FOR v_key IN SELECT jsonb_object_keys(p_filter)
  LOOP
    -- '' (root) or '/...' with valid RFC 6901 escapes.
    IF v_key = '' THEN
      CONTINUE;
    END IF;
    IF v_key !~ '^(/([^/~]|~[01])*)+$' THEN
      RETURN false;
    END IF;
  END LOOP;

  RETURN true;
END;
$_$;


ALTER FUNCTION "automation"."fn_check_filter_keys"("p_filter" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "automation"."fn_cleanup_cron_runs"("p_retention_days" integer DEFAULT 30) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  deleted_count integer;
BEGIN
  DELETE FROM automation.cron_runs
  WHERE started_at < now() - (p_retention_days || ' days')::interval;

  GET DIAGNOSTICS deleted_count = ROW_COUNT;
  RETURN deleted_count;
END $$;


ALTER FUNCTION "automation"."fn_cleanup_cron_runs"("p_retention_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_cleanup_cron_runs"("p_retention_days" integer) IS 'Z11: deletes automation.cron_runs rows older than p_retention_days (default 30). Called daily by the cleanup-cron-runs pg_cron job.';



CREATE OR REPLACE FUNCTION "automation"."fn_dispatch_action"("p_event_id" "uuid", "p_rule_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'lenses', 'audit', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_event   automation.events%ROWTYPE;
  v_rule    automation.trigger_rules%ROWTYPE;
  v_run_id  uuid;
  v_outbox_id uuid;
  v_url     text;
  v_payload jsonb;
  v_title   text;
  v_body    text;
BEGIN
  SELECT * INTO v_event FROM automation.events        WHERE id = p_event_id;
  IF NOT FOUND THEN
    UPDATE automation.event_dispatches
    SET    status = 'skipped', error = 'event_not_found', attempted_at = now()
    WHERE  event_id = p_event_id AND rule_id = p_rule_id;
    RETURN;
  END IF;

  SELECT * INTO v_rule  FROM automation.trigger_rules WHERE id = p_rule_id;
  IF NOT FOUND OR NOT v_rule.is_active THEN
    UPDATE automation.event_dispatches
    SET    status = 'skipped', error = 'rule_inactive_or_missing', attempted_at = now()
    WHERE  event_id = p_event_id AND rule_id = p_rule_id;
    RETURN;
  END IF;

  BEGIN
    IF v_rule.action_kind = 'dispatch_workflow' THEN
      INSERT INTO lenses.workflow_runs (
        workflow_id,
        triggered_by,
        status,
        context_inputs,
        global_model_id,
        trigger_mode,
        metadata
      ) VALUES (
        (v_rule.action_config->>'workflow_id')::uuid,
        v_rule.lenser_id,
        'pending',
        COALESCE(v_rule.action_config->'context_inputs', '{}'::jsonb),
        NULLIF(v_rule.action_config->>'global_model_id', ''),
        'manual',
        jsonb_build_object(
          'origin',          'automation',
          'trigger_rule_id', v_rule.id,
          'event_id',        v_event.id,
          'event_type',      v_event.event_type
        )
      )
      RETURNING id INTO v_run_id;

      INSERT INTO lenses.workflow_node_results (run_id, node_id, status)
      SELECT v_run_id, n.id, 'pending'
      FROM   lenses.workflow_nodes n
      WHERE  n.workflow_id = (v_rule.action_config->>'workflow_id')::uuid;

      UPDATE automation.event_dispatches
      SET    status = 'dispatched', error = NULL, attempted_at = now()
      WHERE  event_id = p_event_id AND rule_id = p_rule_id;

    ELSIF v_rule.action_kind = 'webhook' THEN
      v_url := v_rule.action_config->>'url';
      IF v_url IS NULL OR v_url = '' THEN
        UPDATE automation.event_dispatches
        SET    status = 'failed', error = 'webhook_url_missing', attempted_at = now()
        WHERE  event_id = p_event_id AND rule_id = p_rule_id;
        RETURN;
      END IF;

      v_payload := jsonb_build_object(
        'webhook_version', 1,
        'event_type',      v_event.event_type,
        'event_id',        v_event.id,
        'rule_id',         v_rule.id,
        'occurred_at',     v_event.occurred_at,
        'payload',         v_event.payload
      );

      INSERT INTO audit.webhook_outbox (event_type, payload, target_url)
      VALUES ('automation.' || v_event.event_type, v_payload, v_url)
      RETURNING id INTO v_outbox_id;

      UPDATE automation.event_dispatches
      SET    status     = 'dispatched',
             error      = NULL,
             outbox_id  = v_outbox_id,
             attempted_at = now()
      WHERE  event_id = p_event_id AND rule_id = p_rule_id;

    ELSIF v_rule.action_kind = 'notify' THEN
      v_title := COALESCE(v_rule.action_config->>'title',
                          'Automation: ' || v_event.event_type);
      v_body  := v_rule.action_config->>'body';

      PERFORM public.fn_create_automation_notification(
        v_rule.lenser_id,
        v_title,
        v_body,
        jsonb_build_object(
          'event_type',      v_event.event_type,
          'event_id',        v_event.id,
          'trigger_rule_id', v_rule.id,
          'source_id',       v_event.source_id,
          'template',        v_rule.action_config->>'template'
        )
      );

      UPDATE automation.event_dispatches
      SET    status = 'dispatched', error = NULL, attempted_at = now()
      WHERE  event_id = p_event_id AND rule_id = p_rule_id;

    ELSE
      UPDATE automation.event_dispatches
      SET    status = 'failed',
             error  = 'unknown_action_kind:' || v_rule.action_kind,
             attempted_at = now()
      WHERE  event_id = p_event_id AND rule_id = p_rule_id;
    END IF;

  EXCEPTION WHEN OTHERS THEN
    UPDATE automation.event_dispatches
    SET    status = 'failed',
           error  = left(SQLERRM, 1000),
           attempted_at = now()
    WHERE  event_id = p_event_id AND rule_id = p_rule_id;
  END;
END;
$$;


ALTER FUNCTION "automation"."fn_dispatch_action"("p_event_id" "uuid", "p_rule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_dispatch_action"("p_event_id" "uuid", "p_rule_id" "uuid") IS 'Phase U2: execute the action for one (event, rule) pair. Wraps the entire side-effect in BEGIN...EXCEPTION so a failed action marks the dispatch row failed instead of bubbling up and aborting fn_run_dispatcher''s batch loop.';



CREATE OR REPLACE FUNCTION "automation"."fn_emit_event"("p_type" "text", "p_source_schema" "text", "p_source_table" "text", "p_source_id" "uuid", "p_payload" "jsonb" DEFAULT '{}'::"jsonb", "p_source_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_id uuid;
BEGIN
  INSERT INTO automation.events (
    event_type, source_schema, source_table, source_id,
    source_lenser_id, payload
  )
  VALUES (
    p_type, p_source_schema, p_source_table, p_source_id,
    p_source_lenser_id, COALESCE(p_payload, '{}'::jsonb)
  )
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "automation"."fn_emit_event"("p_type" "text", "p_source_schema" "text", "p_source_table" "text", "p_source_id" "uuid", "p_payload" "jsonb", "p_source_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_emit_event"("p_type" "text", "p_source_schema" "text", "p_source_table" "text", "p_source_id" "uuid", "p_payload" "jsonb", "p_source_lenser_id" "uuid") IS 'Phase U event emitter. SECURITY DEFINER. Callable from producer triggers only — direct invocation by authenticated users is revoked to prevent event spoofing (Phase U security hardening, 20270410200000).';



CREATE OR REPLACE FUNCTION "automation"."fn_eval_filter"("p_filter" "jsonb", "p_payload" "jsonb") RETURNS boolean
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'automation', 'pg_catalog'
    AS $$
DECLARE
  v_pointer  text;
  v_clause   jsonb;
  v_op       text;
  v_operand  jsonb;
  v_actual   jsonb;
  v_path     jsonpath;
BEGIN
  IF p_filter IS NULL OR p_filter = '{}'::jsonb THEN
    RETURN true;
  END IF;

  IF p_payload IS NULL THEN
    p_payload := '{}'::jsonb;
  END IF;

  FOR v_pointer, v_clause IN SELECT key, value FROM jsonb_each(p_filter) LOOP
    BEGIN
      v_path := automation.fn_jsonpointer_to_jsonpath(v_pointer);
    EXCEPTION WHEN OTHERS THEN
      RETURN false;
    END;

    v_actual := jsonb_path_query_first(p_payload, v_path);

    -- Each clause must itself be an object with exactly one operator key.
    IF jsonb_typeof(v_clause) <> 'object' THEN
      RETURN false;
    END IF;

    SELECT key, value INTO v_op, v_operand
    FROM   jsonb_each(v_clause)
    LIMIT  1;

    IF v_op = 'eq' THEN
      IF v_actual IS DISTINCT FROM v_operand THEN
        RETURN false;
      END IF;

    ELSIF v_op = 'neq' THEN
      IF v_actual IS NOT DISTINCT FROM v_operand THEN
        RETURN false;
      END IF;

    ELSIF v_op = 'gt' THEN
      IF v_actual IS NULL
         OR jsonb_typeof(v_actual)  <> 'number'
         OR jsonb_typeof(v_operand) <> 'number' THEN
        RETURN false;
      END IF;
      IF NOT ((v_actual)::text::numeric > (v_operand)::text::numeric) THEN
        RETURN false;
      END IF;

    ELSIF v_op = 'lt' THEN
      IF v_actual IS NULL
         OR jsonb_typeof(v_actual)  <> 'number'
         OR jsonb_typeof(v_operand) <> 'number' THEN
        RETURN false;
      END IF;
      IF NOT ((v_actual)::text::numeric < (v_operand)::text::numeric) THEN
        RETURN false;
      END IF;

    ELSIF v_op = 'contains' THEN
      IF v_actual IS NULL THEN
        RETURN false;
      END IF;

      IF jsonb_typeof(v_actual) = 'string' AND jsonb_typeof(v_operand) = 'string' THEN
        IF position(
             (v_operand #>> '{}') IN (v_actual #>> '{}')
           ) = 0 THEN
          RETURN false;
        END IF;

      ELSIF jsonb_typeof(v_actual) = 'array' THEN
        IF NOT (v_actual @> jsonb_build_array(v_operand)) THEN
          RETURN false;
        END IF;

      ELSE
        RETURN false;
      END IF;

    ELSE
      -- Unknown operator → conservative reject.
      RETURN false;
    END IF;
  END LOOP;

  RETURN true;
END;
$$;


ALTER FUNCTION "automation"."fn_eval_filter"("p_filter" "jsonb", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_eval_filter"("p_filter" "jsonb", "p_payload" "jsonb") IS 'Phase U2: evaluator for the trigger-rule filter DSL v1. AND-of-clauses; unknown ops fail closed. See 20270410100000_phase_u_trigger_rules.sql header for the grammar.';



CREATE OR REPLACE FUNCTION "automation"."fn_jsonpointer_to_jsonpath"("p_pointer" "text") RETURNS "jsonpath"
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'pg_catalog'
    AS $_$
DECLARE
  v_seg     text;
  v_parts   text[];
  v_buf     text := '$';
BEGIN
  -- "" → root.
  IF p_pointer IS NULL OR p_pointer = '' OR p_pointer = '/' THEN
    RETURN '$'::jsonpath;
  END IF;

  -- Strip a leading slash; split the remaining string on '/'.
  v_parts := string_to_array(regexp_replace(p_pointer, '^/', ''), '/');

  FOREACH v_seg IN ARRAY v_parts LOOP
    -- RFC 6901 unescape: '~1' → '/'  then  '~0' → '~'.
    v_seg := replace(replace(v_seg, '~1', '/'), '~0', '~');

    IF v_seg ~ '^[0-9]+$' THEN
      v_buf := v_buf || '[' || v_seg || ']';
    ELSE
      -- Quote keys that aren't bare identifiers; jsonpath grammar allows
      -- $."weird key" form.
      v_buf := v_buf || '."' || replace(v_seg, '"', '\"') || '"';
    END IF;
  END LOOP;

  RETURN v_buf::jsonpath;
END;
$_$;


ALTER FUNCTION "automation"."fn_jsonpointer_to_jsonpath"("p_pointer" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_jsonpointer_to_jsonpath"("p_pointer" "text") IS 'Phase U2: translate an RFC-6901 JSON Pointer (e.g. "/a/b/0") into a jsonpath value usable by jsonb_path_query_first. Limitations: no slice / wildcard / predicate support — pointers only. Reserved chars handled per RFC 6901: ~0 → ~, ~1 → /.';



CREATE OR REPLACE FUNCTION "automation"."fn_match_rules_for_event"("p_event_id" "uuid") RETURNS TABLE("rule_id" "uuid", "action_kind" "text", "action_config" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_event automation.events%ROWTYPE;
BEGIN
  SELECT * INTO v_event FROM automation.events WHERE id = p_event_id;
  IF NOT FOUND THEN
    RETURN;
  END IF;

  -- Insert dispatch ledger rows for matching rules (idempotent).
  -- A rule matches when:
  --   match_event_type = event.event_type
  --   AND is_active
  --   AND ( source_lenser_id IS NULL  -- system-level event, owner-scoping skipped
  --         OR rule.lenser_id = source_lenser_id )
  --   AND fn_eval_filter(match_filter, payload)
  INSERT INTO automation.event_dispatches (event_id, rule_id, status)
  SELECT v_event.id, tr.id, 'queued'
  FROM   automation.trigger_rules tr
  WHERE  tr.is_active
    AND  tr.match_event_type = v_event.event_type
    AND  (v_event.source_lenser_id IS NULL OR tr.lenser_id = v_event.source_lenser_id)
    AND  automation.fn_eval_filter(tr.match_filter, v_event.payload)
  ON CONFLICT (event_id, rule_id) DO NOTHING;

  RETURN QUERY
  SELECT tr.id, tr.action_kind, tr.action_config
  FROM   automation.trigger_rules tr
  JOIN   automation.event_dispatches ed
         ON ed.rule_id  = tr.id
        AND ed.event_id = v_event.id
  WHERE  ed.status = 'queued';
END;
$$;


ALTER FUNCTION "automation"."fn_match_rules_for_event"("p_event_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_match_rules_for_event"("p_event_id" "uuid") IS 'Phase U2: match active rules for the given event, insert (event_id, rule_id, status=queued) rows ON CONFLICT DO NOTHING for idempotency, and return the newly-queued matches. Rules with NULL source_lenser_id on the event are matched without ownership filtering (system events).';



CREATE OR REPLACE FUNCTION "automation"."fn_run_dispatcher"("p_limit" integer DEFAULT 100) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_event_id uuid;
  v_match    record;
  v_actions  int := 0;
BEGIN
  -- Phase 1: events that haven't been routed yet (no dispatch rows at all).
  FOR v_event_id IN
    SELECT e.id
    FROM   automation.events e
    WHERE  NOT EXISTS (
             SELECT 1 FROM automation.event_dispatches ed WHERE ed.event_id = e.id
           )
      AND  EXISTS (
             SELECT 1 FROM automation.trigger_rules tr
             WHERE  tr.is_active
               AND  tr.match_event_type = e.event_type
           )
    ORDER BY e.recorded_at ASC
    LIMIT  p_limit
    FOR UPDATE OF e SKIP LOCKED
  LOOP
    FOR v_match IN
      SELECT rule_id FROM automation.fn_match_rules_for_event(v_event_id)
    LOOP
      PERFORM automation.fn_dispatch_action(v_event_id, v_match.rule_id);
      v_actions := v_actions + 1;
    END LOOP;
  END LOOP;

  -- Phase 2: re-drive dispatch rows that are still in 'queued' state (e.g. the
  -- previous tick crashed between INSERT-queued and dispatch_action).
  FOR v_match IN
    SELECT event_id, rule_id
    FROM   automation.event_dispatches
    WHERE  status = 'queued'
    ORDER  BY attempted_at ASC
    LIMIT  p_limit
    FOR UPDATE SKIP LOCKED
  LOOP
    PERFORM automation.fn_dispatch_action(v_match.event_id, v_match.rule_id);
    v_actions := v_actions + 1;
  END LOOP;

  RETURN v_actions;
END;
$$;


ALTER FUNCTION "automation"."fn_run_dispatcher"("p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_run_dispatcher"("p_limit" integer) IS 'Phase U2: dispatcher entrypoint. Two phases: (1) match new events with active rules, (2) re-drive any dispatch rows still in queued state. Returns number of dispatch_action invocations. Cron job automation-dispatcher runs this once per minute.';



CREATE OR REPLACE FUNCTION "automation"."fn_run_with_lock"("p_job_name" "text", "p_sql" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  got_lock boolean;
  run_id   bigint;
BEGIN
  got_lock := pg_try_advisory_xact_lock(hashtext('cron:' || p_job_name)::bigint);

  IF NOT got_lock THEN
    INSERT INTO automation.cron_runs (job_name, status, finished_at)
      VALUES (p_job_name, 'skipped_locked', now());
    RETURN;
  END IF;

  -- Hard-cap each cron body to 55 s. pg_cron fires at most once per minute;
  -- 55 s leaves 5 s of slack before the next tick. The timeout is
  -- transaction-local and released automatically when the function returns.
  PERFORM set_config('statement_timeout', '55000', true);

  INSERT INTO automation.cron_runs (job_name) VALUES (p_job_name) RETURNING id INTO run_id;
  BEGIN
    EXECUTE p_sql;
    UPDATE automation.cron_runs
      SET status = 'ok', finished_at = now()
      WHERE id = run_id;
  EXCEPTION WHEN others THEN
    UPDATE automation.cron_runs
      SET status = 'error', finished_at = now(), error_msg = SQLERRM
      WHERE id = run_id;
    RAISE;
  END;
END $$;


ALTER FUNCTION "automation"."fn_run_with_lock"("p_job_name" "text", "p_sql" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."fn_run_with_lock"("p_job_name" "text", "p_sql" "text") IS 'Z10: advisory-lock wrapper for pg_cron jobs. Sets statement_timeout=55s after acquiring lock to cap each job body below the 1-min tick window.';



CREATE OR REPLACE FUNCTION "automation"."trg_emit_approval_granted"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_owner_lenser_id uuid;
BEGIN
  IF OLD.approval_status IS DISTINCT FROM NEW.approval_status
     AND NEW.approval_status = 'approved' THEN

    SELECT profile_id INTO v_owner_lenser_id
    FROM   agents.ai_lensers
    WHERE  id = NEW.ai_lenser_id;

    PERFORM automation.fn_emit_event(
      'approval.granted',
      'agents',
      'team_runs',
      NEW.id,
      jsonb_build_object(
        'ai_lenser_id',    NEW.ai_lenser_id,
        'team_id',         NEW.team_id,
        'workflow_id',     NEW.workflow_id,
        'workflow_run_id', NEW.workflow_run_id,
        'granted_at',      NEW.updated_at
      ),
      v_owner_lenser_id
    );
  END IF;
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_emit_approval_granted failed for team_run %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "automation"."trg_emit_approval_granted"() OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."trg_emit_approval_granted"() IS 'Phase U1 producer: emits approval.granted into automation.events when agents.team_runs.approval_status transitions to approved (set by public.fn_decide_approval). Reason / modifications are NOT included — they may contain operator commentary; consumers re-read team_runs.metadata.';



CREATE OR REPLACE FUNCTION "automation"."trg_emit_approval_timed_out"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_owner_lenser_id uuid;
BEGIN
  IF OLD.approval_status IS DISTINCT FROM NEW.approval_status
     AND NEW.approval_status = 'timed_out' THEN

    -- Resolve the AI lenser's profile_id so source_lenser_id matches the
    -- pattern used by other producers (the lenser the rule owner expects).
    SELECT profile_id INTO v_owner_lenser_id
    FROM   agents.ai_lensers
    WHERE  id = NEW.ai_lenser_id;

    PERFORM automation.fn_emit_event(
      'approval.timed_out',
      'agents',
      'team_runs',
      NEW.id,
      jsonb_build_object(
        'ai_lenser_id',     NEW.ai_lenser_id,
        'team_id',          NEW.team_id,
        'workflow_id',      NEW.workflow_id,
        'workflow_run_id',  NEW.workflow_run_id,
        'expired_at',       NEW.completed_at
      ),
      v_owner_lenser_id
    );
  END IF;
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_emit_approval_timed_out failed for team_run %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "automation"."trg_emit_approval_timed_out"() OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."trg_emit_approval_timed_out"() IS 'Phase U1 producer: emits approval.timed_out into automation.events when agents.team_runs.approval_status transitions to timed_out (set by public.fn_expire_stale_approvals). Payload contains IDs only — the gate metadata (which may contain operator notes) is intentionally omitted.';



CREATE OR REPLACE FUNCTION "automation"."trg_emit_battle_finalized"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
BEGIN
  IF TG_OP = 'UPDATE'
     AND OLD.status IS DISTINCT FROM NEW.status
     AND NEW.status = 'closed' THEN
    PERFORM automation.fn_emit_event(
      'battle.finalized',
      'battles',
      'battles',
      NEW.id,
      jsonb_build_object(
        'winner_contender_id', NEW.winner_contender_id,
        'finalized_at',        NEW.finalized_at,
        'is_draw',             (NEW.winner_contender_id IS NULL)
      ),
      NEW.creator_lenser_id
    );
  END IF;
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_emit_battle_finalized failed for battle %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "automation"."trg_emit_battle_finalized"() OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."trg_emit_battle_finalized"() IS 'Phase U1 producer: emits battle.finalized into automation.events when battles.battles.status transitions to closed. Payload contains winner contender id and finalized_at — no titles or prompts (no PII).';



CREATE OR REPLACE FUNCTION "automation"."trg_emit_moderation_flagged"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
BEGIN
  IF NEW.decision_type = 'flagged' THEN
    PERFORM automation.fn_emit_event(
      'battle.flagged',
      NEW.target_entity_schema,
      NEW.target_entity_table,
      NEW.target_entity_id,
      jsonb_build_object(
        'decision_id',     NEW.id,
        'is_ai_moderated', NEW.is_ai_moderated,
        'occurred_at',     NEW.occurred_at
      ),
      NEW.moderator_lenser_id
    );
  END IF;
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_emit_moderation_flagged failed for decision %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "automation"."trg_emit_moderation_flagged"() OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."trg_emit_moderation_flagged"() IS 'Phase U1 producer: emits battle.flagged into automation.events for any audit.moderation_decisions INSERT with decision_type=flagged. Reason text is intentionally NOT included in the payload — it can contain operator commentary; consumers re-read audit.moderation_decisions when needed.';



CREATE OR REPLACE FUNCTION "automation"."trg_emit_workflow_run_state"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'automation', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_event_type text;
BEGIN
  IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
    RETURN NEW;
  END IF;

  IF NEW.status = 'completed' THEN
    v_event_type := 'workflow_run.completed';
  ELSIF NEW.status IN ('failed', 'timed_out') THEN
    v_event_type := 'workflow_run.failed';
  ELSE
    RETURN NEW;
  END IF;

  PERFORM automation.fn_emit_event(
    v_event_type,
    'lenses',
    'workflow_runs',
    NEW.id,
    jsonb_build_object(
      'workflow_id',  NEW.workflow_id,
      'status',       NEW.status,
      'trigger_mode', NEW.trigger_mode,
      'started_at',   NEW.started_at,
      'completed_at', NEW.completed_at
    ),
    NEW.triggered_by
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_emit_workflow_run_state failed for run %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "automation"."trg_emit_workflow_run_state"() OWNER TO "postgres";


COMMENT ON FUNCTION "automation"."trg_emit_workflow_run_state"() IS 'Phase U1 producer: emits workflow_run.completed / workflow_run.failed into automation.events on lenses.workflow_runs status transitions. Failure also fires for status=timed_out (terminal failure mode introduced by Phase K). Payload contains IDs and timestamps; never node outputs or prompt text.';



CREATE OR REPLACE FUNCTION "automation"."trg_trigger_rules_touch_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'automation', 'pg_catalog'
    AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "automation"."trg_trigger_rules_touch_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."award_battle_xp"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'xp', 'lensers'
    AS $$
DECLARE
    v_contender RECORD;
    v_voter     RECORD;
BEGIN
    -- Only fire when status transitions to 'closed'
    IF NEW.status <> 'closed' OR OLD.status = 'closed' THEN
        RETURN NEW;
    END IF;

    -- Award XP to all human contenders (use contender_ref_id directly)
    FOR v_contender IN
        SELECT c.contender_ref_id AS lenser_id
        FROM battles.contenders c
        WHERE c.battle_id = NEW.id
          AND c.contender_type = 'human'
          AND c.contender_ref_id IS NOT NULL
    LOOP
        PERFORM xp.apply(
            p_lenser_id       := v_contender.lenser_id,
            p_rule_key        := 'BATTLE_PARTICIPATED',
            p_source          := 'battle'::xp.source_enum,
            p_source_ref_type := 'battle',
            p_source_ref_id   := NEW.id,
            p_app_id          := '00000000-0000-0000-0000-000000000002'::uuid
        );
    END LOOP;

    -- Award XP to the winner (if human)
    IF NEW.winner_contender_id IS NOT NULL THEN
        DECLARE
            v_winner RECORD;
        BEGIN
            SELECT
                c.contender_ref_id AS lenser_id,
                c.contender_type::text AS resolved_type
            INTO v_winner
            FROM battles.contenders c
            WHERE c.id = NEW.winner_contender_id;

            IF v_winner.resolved_type = 'human' AND v_winner.lenser_id IS NOT NULL THEN
                PERFORM xp.apply(
                    p_lenser_id       := v_winner.lenser_id,
                    p_rule_key        := 'BATTLE_WON',
                    p_source          := 'battle'::xp.source_enum,
                    p_source_ref_type := 'battle',
                    p_source_ref_id   := NEW.id,
                    p_app_id          := '00000000-0000-0000-0000-000000000002'::uuid
                );
            END IF;
        END;
    END IF;

    -- Award XP to all voters
    FOR v_voter IN
        SELECT v.voter_lenser_id AS lenser_id
        FROM battles.votes v
        WHERE v.battle_id = NEW.id
    LOOP
        PERFORM xp.apply(
            p_lenser_id       := v_voter.lenser_id,
            p_rule_key        := 'BATTLE_VOTED',
            p_source          := 'battle'::xp.source_enum,
            p_source_ref_type := 'battle',
            p_source_ref_id   := NEW.id,
            p_app_id          := '00000000-0000-0000-0000-000000000002'::uuid
        );
    END LOOP;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."award_battle_xp"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_advance_tournament"("p_match_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_match       battles.tournament_matches;
  v_battle      battles.battles;
  v_winner_tc   battles.tournament_contenders;
  v_contender_a battles.contenders;
  v_contender_b battles.contenders;
  v_winner_id   UUID;
  v_pending     INT;
  v_round       battles.tournament_rounds;
BEGIN
  SELECT * INTO v_match FROM battles.tournament_matches WHERE id = p_match_id;
  IF NOT FOUND THEN RAISE EXCEPTION 'Match not found: %', p_match_id; END IF;

  SELECT * INTO v_battle FROM battles.battles WHERE id = v_match.battle_id;

  -- Determine winner from battle result
  IF v_battle.winner_contender_id IS NOT NULL THEN
    -- Find which tournament contender maps to the winner
    SELECT tc.id INTO v_winner_id
    FROM   battles.tournament_contenders tc
    JOIN   battles.contenders c ON c.battle_id = v_battle.id
    WHERE  c.id = v_battle.winner_contender_id
    AND    tc.tournament_id = v_match.tournament_id
    LIMIT 1;
  END IF;

  IF v_winner_id IS NOT NULL THEN
    UPDATE battles.tournament_matches
    SET winner_tournament_contender_id = v_winner_id
    WHERE id = p_match_id;

    UPDATE battles.tournament_contenders
    SET status = 'eliminated'
    WHERE tournament_id = v_match.tournament_id
    AND   id IN (v_match.contender_a_id, v_match.contender_b_id)
    AND   id <> v_winner_id;
  END IF;

  -- Check if round is complete
  SELECT * INTO v_round FROM battles.tournament_rounds WHERE id = v_match.round_id;

  SELECT COUNT(*) INTO v_pending
  FROM   battles.tournament_matches
  WHERE  round_id    = v_match.round_id
  AND    winner_tournament_contender_id IS NULL;

  IF v_pending = 0 THEN
    UPDATE battles.tournament_rounds SET status = 'completed' WHERE id = v_match.round_id;

    -- Check if tournament is complete (only one non-eliminated contender)
    SELECT COUNT(*) INTO v_pending
    FROM   battles.tournament_contenders
    WHERE  tournament_id = v_match.tournament_id
    AND    status        = 'active';

    IF v_pending <= 1 THEN
      UPDATE battles.tournament_contenders
      SET status = 'winner'
      WHERE tournament_id = v_match.tournament_id AND status = 'active';

      UPDATE battles.tournaments
      SET status = 'completed', updated_at = now()
      WHERE id = v_match.tournament_id;
    ELSE
      -- Create next round (simple: pair remaining active contenders)
      PERFORM battles.fn_create_next_tournament_round(v_match.tournament_id, v_round.round_number + 1);
    END IF;
  END IF;
END;
$$;


ALTER FUNCTION "battles"."fn_advance_tournament"("p_match_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_auto_close_voting"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  r      RECORD;
  closed INT := 0;
BEGIN
  FOR r IN
    SELECT id
    FROM   battles.battles
    WHERE  status          = 'voting'
    AND    voting_closes_at < now()
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      UPDATE battles.battles
      SET    status = 'scoring'
      WHERE  id = r.id;

      INSERT INTO audit.events (event_type, payload)
      VALUES ('battle.voting_auto_closed', jsonb_build_object('battle_id', r.id));

      closed := closed + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_auto_close_voting: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN closed;
END;
$$;


ALTER FUNCTION "battles"."fn_auto_close_voting"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_auto_close_voting_safe"() RETURNS "void"
    LANGUAGE "sql"
    SET "search_path" TO 'battles', 'automation', 'pg_catalog'
    AS $$
        SELECT automation.fn_run_with_lock(
          'auto-close-voting',
          'SELECT battles.fn_auto_close_voting()'
        );
      $$;


ALTER FUNCTION "battles"."fn_auto_close_voting_safe"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_auto_finalize_battles"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  r           RECORD;
  finalized   INT := 0;
BEGIN
  FOR r IN
    SELECT b.id, b.ai_judge_enabled
    FROM   battles.battles b
    WHERE  b.status = 'scoring'
    AND    b.voting_closes_at < now()
  LOOP
    IF r.ai_judge_enabled AND
       EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net') AND
       current_setting('app.supabase_url', true) IS NOT NULL
    THEN
      PERFORM net.http_post(
        url     := current_setting('app.supabase_url') || '/functions/v1/ai-judge-battle',
        headers := jsonb_build_object(
          'Content-Type',  'application/json',
          'Authorization', 'Bearer ' || internal.get_service_role_key()
        ),
        body    := jsonb_build_object('battle_id', r.id)
      );
    ELSE
      PERFORM public.fn_battles_finalize(r.id);
    END IF;
    finalized := finalized + 1;
  END LOOP;
  RETURN finalized;
END;
$$;


ALTER FUNCTION "battles"."fn_auto_finalize_battles"() OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_auto_finalize_battles"() IS 'Called by pg_cron every 5 minutes. Phase 1: voting→scoring for expired battles. Phase 2: scoring→closed via fn_battles_finalize. Returns count finalized. Errors per-battle are caught and logged to audit.events.';



CREATE OR REPLACE FUNCTION "battles"."fn_auto_finalize_battles_safe"() RETURNS "void"
    LANGUAGE "sql"
    SET "search_path" TO 'battles', 'automation', 'pg_catalog'
    AS $$
        SELECT automation.fn_run_with_lock(
          'auto-finalize-battles',
          'SELECT battles.fn_auto_finalize_battles()'
        );
      $$;


ALTER FUNCTION "battles"."fn_auto_finalize_battles_safe"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_auto_publish_battles"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  r         RECORD;
  published INT := 0;
BEGIN
  FOR r IN
    SELECT id
    FROM   battles.battles
    WHERE  status       = 'closed'
    AND    auto_publish = TRUE
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      PERFORM battles.fn_battles_publish_internal(r.id);
      published := published + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_auto_publish_battles: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN published;
END;
$$;


ALTER FUNCTION "battles"."fn_auto_publish_battles"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_auto_start_battles"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  r          RECORD;
  started    INT := 0;
  cont       RECORD;
BEGIN
  FOR r IN
    SELECT b.id, b.voting_duration_hours
    FROM   battles.battles b
    WHERE  b.status = 'open'
    AND    b.execution_starts_at <= now()
    AND    b.battle_type IN ('ai_vs_ai', 'workflow_battle')
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      -- Transition to executing
      UPDATE battles.battles
      SET    status               = 'executing',
             executing_started_at = now()
      WHERE  id = r.id;

      -- Create one job per contender
      FOR cont IN
        SELECT id, slot
        FROM   battles.contenders
        WHERE  battle_id = r.id
        ORDER  BY slot
      LOOP
        INSERT INTO battles.battle_execution_jobs
          (battle_id, contender_id, slot, status)
        VALUES
          (r.id, cont.id, cont.slot, 'queued')
        ON CONFLICT (battle_id, contender_id) DO NOTHING;
      END LOOP;

      -- Audit log
      INSERT INTO audit.events (event_type, payload)
      VALUES ('battle.auto_started', jsonb_build_object('battle_id', r.id));

      started := started + 1;
    EXCEPTION WHEN OTHERS THEN
      -- Per-battle errors must not abort the whole sweep.
      RAISE WARNING 'fn_auto_start_battles: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN started;
END;
$$;


ALTER FUNCTION "battles"."fn_auto_start_battles"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_battle_lifecycle_tick"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'admin', 'audit', 'public', 'lensers'
    AS $$
DECLARE
  r         RECORD;
  sub       RECORD;
  processed INT := 0;
  daily_cnt INT;
BEGIN
  IF public.fn_kill_switch_active('system', NULL) THEN
    RAISE NOTICE 'fn_battle_lifecycle_tick: system kill switch active — skipping';
    RETURN 0;
  END IF;

  -- 1. draft → open + auto-enrollment
  FOR r IN
    SELECT b.id, b.status, b.battle_type
    FROM   battles.battles b
    JOIN   battles.schedules s ON s.battle_id = b.id
    WHERE  b.status      = 'draft'
    AND    s.open_at    <= now()
    AND    b.deleted_at IS NULL
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      CONTINUE WHEN public.fn_kill_switch_active('battle', r.id);

      UPDATE battles.battles
      SET    status = 'open', updated_at = now()
      WHERE  id = r.id;

      INSERT INTO audit.events (event_type, actor_type, severity, payload)
      VALUES ('battle.lifecycle.auto_opened', 'system', 'info',
              jsonb_build_object('battle_id', r.id, 'tick', now()));

      -- Auto-enroll matching subscriptions
      FOR sub IN
        SELECT
          bs.id          AS sub_id,
          bs.agent_id,
          bs.execution_mode,
          bs.workflow_id,
          bs.require_owner_approval,
          bs.max_joins_per_day,
          al.profile_id  AS lenser_id
        FROM   agents.battle_subscriptions bs
        JOIN   agents.ai_lensers al ON al.id = bs.agent_id
        WHERE  bs.active = true
        AND    al.is_active = true
        AND    al.suspended_at IS NULL
        AND    (bs.category IS NULL)  -- TODO: match battle category when field added
      LOOP
        BEGIN
          -- Per-agent kill switch
          CONTINUE WHEN public.fn_kill_switch_active('agent', sub.agent_id);

          -- Daily rate limit
          daily_cnt := public.fn_agent_daily_join_count(sub.agent_id);
          IF daily_cnt >= sub.max_joins_per_day THEN
            INSERT INTO audit.events (event_type, actor_type, severity, payload)
            VALUES ('battle.auto_enroll.rate_limited', 'system', 'info',
                    jsonb_build_object('battle_id', r.id, 'agent_id', sub.agent_id,
                                       'daily_count', daily_cnt, 'max', sub.max_joins_per_day));
            CONTINUE;
          END IF;

          IF sub.require_owner_approval THEN
            -- Create a notification for the owner to approve
            INSERT INTO audit.events (event_type, actor_type, severity, payload)
            VALUES ('battle.auto_enroll.pending_approval', 'system', 'info',
                    jsonb_build_object('battle_id', r.id, 'agent_id', sub.agent_id,
                                       'subscription_id', sub.sub_id));
            CONTINUE;
          END IF;

          -- Auto-join: insert contender if not already registered
          INSERT INTO battles.contenders (battle_id, lenser_id, contender_status)
          SELECT r.id, sub.lenser_id, 'active'
          WHERE NOT EXISTS (
            SELECT 1 FROM battles.contenders
            WHERE battle_id = r.id AND lenser_id = sub.lenser_id
          )
          AND (SELECT count(*) FROM battles.contenders WHERE battle_id = r.id) <
              (SELECT max_contenders FROM battles.battles WHERE id = r.id);

          INSERT INTO audit.events (event_type, actor_type, severity, payload)
          VALUES ('battle.auto_enrolled', 'system', 'info',
                  jsonb_build_object('battle_id', r.id, 'agent_id', sub.agent_id,
                                     'lenser_id', sub.lenser_id));
        EXCEPTION WHEN OTHERS THEN
          RAISE WARNING 'lifecycle_tick.enroll: agent % battle % failed: %', sub.agent_id, r.id, SQLERRM;
        END;
      END LOOP;

      processed := processed + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'lifecycle_tick.open: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  -- 2. scoring → closed (auto-judge)
  FOR r IN
    SELECT b.id
    FROM   battles.battles b
    JOIN   battles.schedules s ON s.battle_id = b.id
    WHERE  b.status     = 'scoring'
    AND    s.auto_judge = true
    AND    s.judge_at  <= now()
    AND    b.deleted_at IS NULL
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      CONTINUE WHEN public.fn_kill_switch_active('battle', r.id);
      UPDATE battles.battles SET status = 'closed', updated_at = now() WHERE id = r.id;
      INSERT INTO audit.events (event_type, actor_type, severity, payload)
      VALUES ('battle.lifecycle.auto_judged', 'system', 'info',
              jsonb_build_object('battle_id', r.id, 'tick', now()));
      processed := processed + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'lifecycle_tick.judge: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  -- 3. closed → published
  FOR r IN
    SELECT b.id
    FROM   battles.battles b
    JOIN   battles.schedules s ON s.battle_id = b.id
    WHERE  b.status        = 'closed'
    AND    s.auto_publish  = true
    AND    s.publish_at   <= now()
    AND    b.deleted_at   IS NULL
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      CONTINUE WHEN public.fn_kill_switch_active('battle', r.id);
      UPDATE battles.battles
      SET    status       = 'published',
             published_at = now(),
             updated_at   = now()
      WHERE  id = r.id;
      INSERT INTO audit.events (event_type, actor_type, severity, payload)
      VALUES ('battle.lifecycle.auto_published', 'system', 'info',
              jsonb_build_object('battle_id', r.id, 'tick', now()));
      processed := processed + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'lifecycle_tick.publish: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN processed;
END;
$$;


ALTER FUNCTION "battles"."fn_battle_lifecycle_tick"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_battles_publish_internal"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_battle  battles.battles;
  v_winner  battles.contenders;
  v_ca      battles.contenders;
  v_cb      battles.contenders;
BEGIN
  UPDATE battles.battles
  SET    status       = 'published',
         published_at = now()
  WHERE  id     = p_battle_id
  AND    status = 'closed';

  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;

  SELECT * INTO v_ca FROM battles.contenders WHERE battle_id = p_battle_id AND slot = 'A' LIMIT 1;
  SELECT * INTO v_cb FROM battles.contenders WHERE battle_id = p_battle_id AND slot = 'B' LIMIT 1;

  IF v_battle.winner_contender_id IS NOT NULL THEN
    SELECT * INTO v_winner FROM battles.contenders WHERE id = v_battle.winner_contender_id;
  END IF;

  -- Fire OG image generation (pg_net, graceful degradation)
  IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net')
     AND current_setting('app.supabase_url', true) IS NOT NULL
  THEN
    PERFORM net.http_post(
      url     := current_setting('app.supabase_url') || '/functions/v1/generate-battle-og-image',
      headers := jsonb_build_object(
        'Content-Type',  'application/json',
        'Authorization', 'Bearer ' || internal.get_service_role_key()
      ),
      body    := jsonb_build_object(
        'battle_id',          p_battle_id,
        'title',              v_battle.title,
        'winner_name',        COALESCE(v_winner.display_name, null),
        'contender_a_name',   COALESCE(v_ca.display_name, 'Contender A'),
        'contender_b_name',   COALESCE(v_cb.display_name, 'Contender B')
      )
    );
  END IF;

  -- Notify participants
  IF EXISTS (
    SELECT 1 FROM pg_proc
    WHERE  proname = 'fn_notify_battle_result'
    AND    pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
  ) THEN
    PERFORM public.fn_notify_battle_result(p_battle_id);
  END IF;

  INSERT INTO audit.events (event_type, payload)
  VALUES ('battle.auto_published', jsonb_build_object('battle_id', p_battle_id));
END;
$$;


ALTER FUNCTION "battles"."fn_battles_publish_internal"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_battles_start_voting_internal"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  UPDATE battles.battles
  SET    status           = 'voting',
         voting_opens_at  = now(),
         voting_closes_at = p_voting_closes_at
  WHERE  id     = p_battle_id
  AND    status = 'executing';

  INSERT INTO audit.events (event_type, payload)
  VALUES ('battle.voting_started_internal', jsonb_build_object(
    'battle_id',        p_battle_id,
    'voting_closes_at', p_voting_closes_at
  ));
END;
$$;


ALTER FUNCTION "battles"."fn_battles_start_voting_internal"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_check_vote_rate_limit"("p_voter_lenser_id" "uuid", "p_battle_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT NOT EXISTS (
    SELECT 1
    FROM battles.votes
    WHERE voter_lenser_id  = p_voter_lenser_id
      AND battle_id = p_battle_id
      AND created_at > (now() - INTERVAL '60 seconds')
  );
$$;


ALTER FUNCTION "battles"."fn_check_vote_rate_limit"("p_voter_lenser_id" "uuid", "p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_claim_battle_execution_job"("p_worker_id" "text") RETURNS TABLE("job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "task_prompt" "text", "provider_key" "text", "model_key" "text", "byok_key_ref_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "max_tokens" integer, "temperature" numeric, "retry_count" integer, "ai_lenser_id" "uuid", "personality_note" "text", "personality_version_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'public'
    AS $$
DECLARE
  v_job battles.battle_execution_jobs;
BEGIN
  -- GUARD: system kill switch blocks all battle job claims immediately.
  IF public.fn_kill_switch_active('system') THEN
    RETURN;  -- empty result set — worker backs off silently
  END IF;

  SELECT j.*
  INTO   v_job
  FROM   battles.battle_execution_jobs j
  WHERE  j.status = 'queued'
    -- Respect backoff: fn_requeue_battle_job_with_backoff encodes the earliest
    -- pickup time in claimed_at. NULL = never been claimed; future = on backoff.
    AND  (j.claimed_at IS NULL OR j.claimed_at <= now())
  ORDER  BY j.created_at
  LIMIT  1
  FOR UPDATE SKIP LOCKED;

  IF NOT FOUND THEN
    RETURN;
  END IF;

  UPDATE battles.battle_execution_jobs
  SET    status     = 'claimed',
         worker_id  = p_worker_id,
         claimed_at = now()
  WHERE  id = v_job.id;

  RETURN QUERY
    SELECT
      v_job.id,
      v_job.battle_id,
      v_job.contender_id,
      v_job.slot,
      b.task_prompt,
      COALESCE(ec.provider_key, ''),
      COALESCE(ec.model_key,    ''),
      ec.byok_key_ref_id,
      cla.lens_id,
      cla.version_id,
      COALESCE(ec.max_tokens,  4096),
      COALESCE(ec.temperature, 0.7),
      v_job.retry_count,
      al.id,
      al.personality_note,
      plb.version_id
    FROM battles.battles b
    LEFT JOIN battles.execution_configs ec
           ON ec.battle_id    = v_job.battle_id
          AND (ec.contender_id = v_job.contender_id OR ec.contender_id IS NULL)
    LEFT JOIN battles.contender_lens_assignments cla
           ON cla.contender_id = v_job.contender_id
    LEFT JOIN battles.contenders con
           ON con.id             = v_job.contender_id
          AND con.contender_type = 'ai_agent'
    LEFT JOIN agents.ai_lensers al
           ON al.profile_id     = con.contender_ref_id
    LEFT JOIN agents.lens_bindings plb
           ON plb.ai_lenser_id  = al.id
          AND plb.is_default    = TRUE
          AND 'personality'     = ANY(plb.category_tags)
    WHERE b.id = v_job.battle_id
    ORDER BY ec.contender_id NULLS LAST
    LIMIT 1;
END;
$$;


ALTER FUNCTION "battles"."fn_claim_battle_execution_job"("p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_claim_battle_execution_job"("p_worker_id" "text") IS 'CU fix (backoff timing) + Emergency Stop: adds kill-switch guard at entry. Returns empty result set when the system kill switch is active, preventing any new battle jobs from being claimed by workers.';



CREATE OR REPLACE FUNCTION "battles"."fn_clone_battle_for_rematch"("p_parent_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_parent          battles.battles%ROWTYPE;
  v_new_id          uuid;
  v_new_slug        text;
  v_invite_code     text;
  v_active_count    int;
  v_slot_count      int;
BEGIN
  SELECT * INTO v_parent
    FROM battles.battles
   WHERE id = p_parent_id
     AND deleted_at IS NULL;

  IF v_parent.id IS NULL THEN
    RAISE EXCEPTION 'parent_battle_not_found: %', p_parent_id
      USING ERRCODE = 'P0002';
  END IF;

  IF v_parent.status NOT IN ('closed','published','archived') THEN
    RAISE EXCEPTION
      'parent_battle_not_terminal: status=% (must be closed/published/archived)',
      v_parent.status
      USING ERRCODE = '22023';
  END IF;

  -- Slug: parent slug + '-r' + 6 random hex chars. Truncate parent prefix to
  -- 200 chars to keep the result well under any reasonable length cap.
  v_new_slug := substr(v_parent.slug, 1, 200) || '-r' ||
                substr(replace(gen_random_uuid()::text, '-', ''), 1, 6);

  v_invite_code := substr(replace(gen_random_uuid()::text, '-', ''), 1, 12);

  INSERT INTO battles.battles (
    creator_lenser_id, title, slug, task_prompt, rubric_id,
    status, invite_code, max_contenders,
    battle_type, voter_eligibility, handicap_config,
    workflow_id, lens_id, content_type,
    parent_battle_id
  )
  VALUES (
    v_parent.creator_lenser_id,
    'Rematch: ' || v_parent.title,
    v_new_slug,
    v_parent.task_prompt,
    v_parent.rubric_id,
    'draft'::battles.battle_status_enum,
    v_invite_code,
    v_parent.max_contenders,
    v_parent.battle_type,
    v_parent.voter_eligibility,
    v_parent.handicap_config,
    v_parent.workflow_id,
    v_parent.lens_id,
    v_parent.content_type,
    v_parent.id
  )
  RETURNING id INTO v_new_id;

  -- Count active parent contenders so we can compute the rotation modulus.
  SELECT count(*) INTO v_active_count
    FROM battles.contenders
   WHERE battle_id = v_parent.id
     AND contender_status = 'active';

  IF v_active_count = 0 THEN
    RETURN v_new_id;
  END IF;

  -- Distinct slots across active contenders (used as rotation modulus).
  -- We rotate within the *occupied* slots, sorted ascending.
  WITH active AS (
    SELECT id, slot, contender_type, contender_ref_id, display_name,
           team_id, entry_mode,
           ROW_NUMBER() OVER (ORDER BY slot ASC) - 1 AS idx,
           COUNT(*)     OVER ()                       AS total
      FROM battles.contenders
     WHERE battle_id = v_parent.id
       AND contender_status = 'active'
  ),
  slot_order AS (
    SELECT slot,
           ROW_NUMBER() OVER (ORDER BY slot ASC) - 1 AS slot_idx
      FROM (
        SELECT DISTINCT slot
          FROM battles.contenders
         WHERE battle_id = v_parent.id
           AND contender_status = 'active'
      ) s
  )
  INSERT INTO battles.contenders (
    battle_id, slot, contender_type, contender_ref_id,
    display_name, team_id, entry_mode, contender_status
  )
  SELECT
    v_new_id,
    -- Rotation: contender at idx i is reseated into the slot at position
    -- (i - 1) mod total, taken from slot_order. For total=2 this is a swap;
    -- for total>=3 this is a left rotation by one position.
    (SELECT slot FROM slot_order
       WHERE slot_idx = ((a.idx - 1 + a.total) % a.total)),
    a.contender_type,
    a.contender_ref_id,
    a.display_name,
    a.team_id,
    a.entry_mode,
    -- Reset lifecycle: the rematch starts fresh, so reseated contenders
    -- enter as 'pending' rather than inheriting 'active'. The owner must
    -- re-open / re-publish the rematch.
    'pending'::text
  FROM active a;

  RETURN v_new_id;
END;
$$;


ALTER FUNCTION "battles"."fn_clone_battle_for_rematch"("p_parent_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_clone_battle_for_rematch"("p_parent_id" "uuid") IS 'Phase V1 internal helper. Clones a terminal-state parent battle (closed/published/archived) into a fresh draft, copies the structural columns, rotates contender slots by one position, and links parent_battle_id. Performs NO ownership check — callers must enforce that. Service role only; the public-facing wrapper is public.fn_battles_create_rematch.';



CREATE OR REPLACE FUNCTION "battles"."fn_complete_battle_execution_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text" DEFAULT NULL::"text", "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_job           battles.battle_execution_jobs;
  v_battle        battles.battles;
  v_pending_count INT;
BEGIN
  SELECT * INTO v_job FROM battles.battle_execution_jobs WHERE id = p_job_id FOR UPDATE;
  IF NOT FOUND THEN RAISE EXCEPTION 'job not found: %', p_job_id; END IF;

  SELECT * INTO v_battle FROM battles.battles WHERE id = v_job.battle_id;

  -- Update job terminal state
  UPDATE battles.battle_execution_jobs
  SET    status       = p_status,
         completed_at = now(),
         error_message = p_error
  WHERE  id = p_job_id;

  -- On success: record the submission
  IF p_status = 'completed' AND p_output_text IS NOT NULL THEN
    INSERT INTO battles.submissions (battle_id, contender_id, content_text, content_url, status)
    VALUES (v_job.battle_id, v_job.contender_id, p_output_text, NULL, 'submitted')
    ON CONFLICT (battle_id, contender_id) DO UPDATE SET content_text = EXCLUDED.content_text, status = 'submitted';
  END IF;

  -- If both jobs are now done (completed or failed), auto-start voting
  SELECT COUNT(*)
  INTO   v_pending_count
  FROM   battles.battle_execution_jobs
  WHERE  battle_id = v_job.battle_id
  AND    status    NOT IN ('completed','failed');

  IF v_pending_count = 0 AND v_battle.status = 'executing' THEN
    PERFORM battles.fn_battles_start_voting_internal(
      v_job.battle_id,
      now() + (v_battle.voting_duration_hours || ' hours')::INTERVAL
    );
  END IF;

  INSERT INTO audit.events (event_type, payload)
  VALUES ('battle.execution_job_completed', jsonb_build_object(
    'job_id',     p_job_id,
    'battle_id',  v_job.battle_id,
    'slot',       v_job.slot,
    'status',     p_status
  ));
END;
$$;


ALTER FUNCTION "battles"."fn_complete_battle_execution_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_create_next_tournament_round"("p_tournament_id" "uuid", "p_round_number" integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  v_tournament  battles.tournaments;
  v_contenders  battles.tournament_contenders[];
  v_round       battles.tournament_rounds;
  i             INT;
  v_a           battles.tournament_contenders;
  v_b           battles.tournament_contenders;
  v_battle      battles.battles;
BEGIN
  SELECT * INTO v_tournament FROM battles.tournaments WHERE id = p_tournament_id;

  SELECT array_agg(tc ORDER BY tc.seed)
  INTO   v_contenders
  FROM   battles.tournament_contenders tc
  WHERE  tc.tournament_id = p_tournament_id AND tc.status = 'active';

  IF array_length(v_contenders, 1) < 2 THEN RETURN; END IF;

  INSERT INTO battles.tournament_rounds (tournament_id, round_number, status)
  VALUES (p_tournament_id, p_round_number, 'active')
  RETURNING * INTO v_round;

  i := 1;
  WHILE i <= array_length(v_contenders, 1) / 2 LOOP
    v_a := v_contenders[i * 2 - 1];
    v_b := v_contenders[i * 2];

    INSERT INTO battles.battles (
      title, task_prompt, battle_type, voter_eligibility, status, slug,
      execution_starts_at, auto_publish, voting_duration_hours,
      ai_judge_enabled, ai_judge_model_key, creator_lenser_id
    )
    SELECT
      v_tournament.title || ' — Round ' || p_round_number || ' Match ' || i,
      'Tournament match',
      v_tournament.battle_type,
      'open', 'open',
      lower(regexp_replace(v_tournament.title, '[^a-zA-Z0-9]', '-', 'g')) || '-r' || p_round_number || 'm' || i,
      now() + INTERVAL '5 minutes',
      TRUE, 24,
      v_tournament.ai_judge_enabled,
      CASE WHEN v_tournament.ai_judge_enabled THEN 'claude-sonnet-4-6' ELSE NULL END,
      v_tournament.creator_lenser_id
    RETURNING * INTO v_battle;

    INSERT INTO battles.tournament_matches
      (tournament_id, round_id, battle_id, contender_a_id, contender_b_id)
    VALUES
      (p_tournament_id, v_round.id, v_battle.id, v_a.id, v_b.id);

    i := i + 1;
  END LOOP;
END;
$$;


ALTER FUNCTION "battles"."fn_create_next_tournament_round"("p_tournament_id" "uuid", "p_round_number" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_create_rematch_internal"("p_parent_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers'
    AS $$
BEGIN
  RETURN battles.fn_clone_battle_for_rematch(p_parent_id);
END;
$$;


ALTER FUNCTION "battles"."fn_create_rematch_internal"("p_parent_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_create_rematch_internal"("p_parent_id" "uuid") IS 'Phase V1/V4: service-role-only entry point that bypasses caller ownership. Used by battles.fn_dispatch_series_rematches to chain a series. NEVER grant this to authenticated.';



CREATE OR REPLACE FUNCTION "battles"."fn_dispatch_recurring_battle_templates"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  t           RECORD;
  new_battle  battles.battles;
  dispatched  INT := 0;
BEGIN
  FOR t IN
    SELECT * FROM battles.templates
    WHERE  recurrence_rule IS NOT NULL
    AND    next_run_at     IS NOT NULL
    AND    next_run_at     <= now()
  LOOP
    -- Create battle from template
    INSERT INTO battles.battles (
      title, task_prompt, battle_type, voter_eligibility,
      status, slug, execution_starts_at, auto_publish,
      voting_duration_hours, creator_lenser_id
    )
    SELECT
      t.title,
      t.task_prompt,
      t.battle_type,
      t.voter_eligibility,
      'open',
      lower(regexp_replace(t.title, '[^a-zA-Z0-9]', '-', 'g')) || '-' || to_char(now(), 'YYYYMMDD-HH24MISS'),
      now() + (t.auto_start_delay_hours || ' hours')::INTERVAL,
      TRUE,
      COALESCE(t.voting_duration_hours, 24),
      t.creator_lenser_id
    RETURNING * INTO new_battle;

    -- Advance next_run_at by a simple daily/weekly interval parsed from recurrence_rule.
    -- Full RRULE parsing is left to application layer; here we support FREQ=DAILY and FREQ=WEEKLY.
    UPDATE battles.templates
    SET next_run_at = CASE
      WHEN recurrence_rule LIKE '%FREQ=DAILY%'  THEN next_run_at + INTERVAL '1 day'
      WHEN recurrence_rule LIKE '%FREQ=WEEKLY%' THEN next_run_at + INTERVAL '7 days'
      WHEN recurrence_rule LIKE '%FREQ=HOURLY%' THEN next_run_at + INTERVAL '1 hour'
      ELSE NULL
    END
    WHERE id = t.id;

    dispatched := dispatched + 1;
  END LOOP;
  RETURN dispatched;
END;
$$;


ALTER FUNCTION "battles"."fn_dispatch_recurring_battle_templates"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_dispatch_series_rematches"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers', 'lenses'
    AS $$
DECLARE
  v_series       record;
  v_parent_id    uuid;
  v_new_id       uuid;
  v_next_pos     int;
  v_count        int := 0;
  v_now          timestamptz := now();
BEGIN
  FOR v_series IN
    SELECT id, seed_battle_id, cron_expr
      FROM battles.series
     WHERE is_active = true
       AND next_dispatch_at <= v_now
     ORDER BY next_dispatch_at ASC
  LOOP
    BEGIN
      -- Cron-gate. If the expression doesn't match this hour, advance the
      -- pointer and move on.
      IF NOT lenses.fn_cron_matches_now(v_series.cron_expr, v_now) THEN
        UPDATE battles.series
           SET next_dispatch_at = date_trunc('hour', v_now) + interval '1 hour'
         WHERE id = v_series.id;
        CONTINUE;
      END IF;

      -- Find the parent: most recent member of the chain, fallback to seed.
      SELECT sb.battle_id, sb.position + 1
        INTO v_parent_id, v_next_pos
        FROM battles.series_battles sb
       WHERE sb.series_id = v_series.id
       ORDER BY sb.position DESC
       LIMIT 1;

      IF v_parent_id IS NULL THEN
        v_parent_id := v_series.seed_battle_id;
        v_next_pos  := 1;
      END IF;

      -- Spawn the rematch via the service-role internal entry point. This
      -- bypasses the caller-ownership check, which is correct here because
      -- the dispatcher runs as service_role and only iterates series that
      -- their owners explicitly created.
      v_new_id := battles.fn_create_rematch_internal(v_parent_id);

      INSERT INTO battles.series_battles (series_id, battle_id, position)
      VALUES (v_series.id, v_new_id, v_next_pos);

      UPDATE battles.series
         SET next_dispatch_at = date_trunc('hour', v_now) + interval '1 hour'
       WHERE id = v_series.id;

      v_count := v_count + 1;

    EXCEPTION WHEN OTHERS THEN
      -- Per-series isolation: never let one bad series block the rest.
      -- Advance the pointer so we don't busy-loop on a permanently broken
      -- series at every dispatcher tick.
      UPDATE battles.series
         SET next_dispatch_at = date_trunc('hour', v_now) + interval '1 hour'
       WHERE id = v_series.id;
      RAISE WARNING 'series_dispatch_failed: series_id=%, sqlstate=%, message=%',
        v_series.id, SQLSTATE, SQLERRM;
    END;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "battles"."fn_dispatch_series_rematches"() OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_dispatch_series_rematches"() IS 'Phase V4 dispatcher. Hourly cron entry point. Advances every active series whose cron expression matches now(), spawning a rematch via battles.fn_create_rematch_internal. Per-series errors are swallowed and logged as WARNING so one bad series cannot block the rest. Service role only.';



CREATE OR REPLACE FUNCTION "battles"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "criterion_id" "uuid", "score" numeric, "rationale" "text", "model_key" "text", "run_id" "uuid", "created_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    v.id,
    v.contender_id,
    v.criterion_id,
    v.score,
    v.rationale,
    v.model_key,
    v.run_id,
    v.created_at
  FROM battles.ai_judge_verdicts v
  WHERE v.battle_id = p_battle_id
    AND EXISTS (
      SELECT 1
      FROM battles.battles b
      WHERE b.id = p_battle_id
        AND b.status IN ('published', 'closed')
    )
  ORDER BY v.contender_id, v.criterion_id NULLS LAST;
$$;


ALTER FUNCTION "battles"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_get_public_execution_jobs"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "status" "text", "claimed_at" timestamp with time zone, "completed_at" timestamp with time zone, "retry_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    j.id,
    j.battle_id,
    j.slot,
    j.status,
    j.claimed_at,
    j.completed_at,
    j.retry_count,
    j.created_at
  FROM battles.battle_execution_jobs j
  WHERE j.battle_id = p_battle_id
    AND EXISTS (
      SELECT 1
      FROM battles.battles b
      WHERE b.id = p_battle_id
        AND b.status IN ('executing', 'voting', 'scoring', 'closed', 'published')
    )
  ORDER BY j.slot;
$$;


ALTER FUNCTION "battles"."fn_get_public_execution_jobs"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_model_test_runs_immutable"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles', 'pg_catalog'
    AS $$
BEGIN
  RAISE EXCEPTION 'model_test_runs is append-only' USING ERRCODE = '42501';
END $$;


ALTER FUNCTION "battles"."fn_model_test_runs_immutable"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text" DEFAULT NULL::"text", "p_error_msg" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  v_job battles.battle_execution_jobs;
  v_dlq_id UUID;
BEGIN
  SELECT * INTO v_job FROM battles.battle_execution_jobs WHERE id = p_job_id;
  IF NOT FOUND THEN RAISE EXCEPTION 'job not found: %', p_job_id; END IF;

  INSERT INTO battles.battle_execution_dead_letters
    (job_id, battle_id, contender_id, slot, error_code, error_message, attempt_count)
  VALUES
    (p_job_id, v_job.battle_id, v_job.contender_id, v_job.slot,
     p_error_code, p_error_msg, v_job.retry_count + 1)
  RETURNING id INTO v_dlq_id;

  UPDATE battles.battle_execution_jobs
  SET    status = 'failed', error_message = p_error_msg
  WHERE  id = p_job_id;

  RETURN v_dlq_id;
END;
$$;


ALTER FUNCTION "battles"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_populate_contender_entity_map"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
BEGIN
  INSERT INTO battles.contender_entity_map (
    contender_id,
    profile_id,
    ai_lenser_id
  )
  VALUES (
    NEW.id,
    CASE WHEN NEW.contender_type = 'human'
         THEN NEW.contender_ref_id
         ELSE NULL END,
    CASE WHEN NEW.contender_type IN ('ai_model', 'ai_agent')
         THEN NEW.contender_ref_id
         ELSE NULL END
  )
  ON CONFLICT (contender_id) DO NOTHING;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_populate_contender_entity_map"() OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_populate_contender_entity_map"() IS 'AFTER INSERT trigger on battles.contenders. Auto-populates battles.contender_entity_map from contender_ref_id + contender_type. Ensures the canonical typed reference is always created with the contender.';



CREATE OR REPLACE FUNCTION "battles"."fn_reclaim_stale_battle_jobs"("p_stale_after_minutes" integer DEFAULT 10) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  r         RECORD;
  reclaimed integer := 0;
BEGIN
  FOR r IN
    SELECT j.id, j.battle_id, j.contender_id, j.slot, j.retry_count, j.max_retries
    FROM   battles.battle_execution_jobs j
    WHERE  j.status     = 'claimed'
      AND  j.claimed_at < now() - (p_stale_after_minutes || ' minutes')::interval
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      IF r.retry_count >= r.max_retries - 1 THEN
        -- Max retries exhausted: move to DLQ.
        UPDATE battles.battle_execution_jobs
        SET    status        = 'failed',
               error_message = 'stale_claim: worker did not complete within timeout',
               completed_at  = now()
        WHERE  id = r.id;

        -- Re-use the existing DLQ function (handles the dead-letter insert).
        PERFORM battles.fn_move_battle_job_to_dlq(
          r.id,
          'stale_claim.max_retries_exceeded',
          format(
            'Worker did not complete job within %s minutes (retry_count=%s, max_retries=%s)',
            p_stale_after_minutes, r.retry_count, r.max_retries
          )
        );
      ELSE
        -- Still have retries left: return to queued immediately (no backoff added
        -- here — the worker will apply exponential backoff on its next failure).
        UPDATE battles.battle_execution_jobs
        SET    status        = 'queued',
               worker_id     = NULL,
               claimed_at    = NULL,
               retry_count   = retry_count + 1,
               error_message = format(
                 'stale_claim: requeued after %s-minute worker timeout (attempt %s)',
                 p_stale_after_minutes, retry_count + 1
               )
        WHERE  id = r.id;
      END IF;

      INSERT INTO audit.events (event_type, payload)
      VALUES (
        'battle.stale_job_reclaimed',
        jsonb_build_object(
          'job_id',      r.id,
          'battle_id',   r.battle_id,
          'slot',        r.slot,
          'retry_count', r.retry_count,
          'to_dlq',      r.retry_count >= r.max_retries - 1
        )
      );

      reclaimed := reclaimed + 1;
    EXCEPTION WHEN OTHERS THEN
      -- Per-job errors must not abort the sweep.
      RAISE WARNING 'fn_reclaim_stale_battle_jobs: job % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN reclaimed;
END;
$$;


ALTER FUNCTION "battles"."fn_reclaim_stale_battle_jobs"("p_stale_after_minutes" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."fn_reclaim_stale_battle_jobs"("p_stale_after_minutes" integer) IS 'CU: Reclaims battle execution jobs stuck in ''claimed'' status when a worker crashes mid-execution. Returns them to ''queued'' (with retry++) or moves them to the DLQ when max_retries is exhausted. Runs every 2 min via pg_cron.';



CREATE OR REPLACE FUNCTION "battles"."fn_record_ai_judge_verdict"("p_battle_id" "uuid", "p_verdicts" "jsonb") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  v        JSONB;
  inserted INT := 0;
BEGIN
  FOR v IN SELECT * FROM jsonb_array_elements(p_verdicts)
  LOOP
    INSERT INTO battles.ai_judge_verdicts
      (battle_id, contender_id, criterion_id, score, rationale, model_key, run_id)
    VALUES (
      p_battle_id,
      (v->>'contender_id')::UUID,
      CASE WHEN v->>'criterion_id' IS NOT NULL THEN (v->>'criterion_id')::UUID ELSE NULL END,
      (v->>'score')::NUMERIC,
      v->>'rationale',
      COALESCE(v->>'model_key', 'claude-sonnet-4-6'),
      CASE WHEN v->>'run_id' IS NOT NULL THEN (v->>'run_id')::UUID ELSE NULL END
    );
    inserted := inserted + 1;
  END LOOP;

  -- Trigger finalization when ai_judge_enabled
  IF EXISTS (
    SELECT 1 FROM battles.battles
    WHERE id = p_battle_id AND ai_judge_enabled = TRUE AND status IN ('scoring', 'voting')
  ) THEN
    PERFORM public.fn_battles_finalize(p_battle_id);
  END IF;

  RETURN inserted;
END;
$$;


ALTER FUNCTION "battles"."fn_record_ai_judge_verdict"("p_battle_id" "uuid", "p_verdicts" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_refresh_all_vote_velocities"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  r        RECORD;
  refreshed INT := 0;
BEGIN
  FOR r IN
    SELECT DISTINCT b.id
    FROM   battles.battles b
    JOIN   battles.votes v ON v.battle_id = b.id
    WHERE  b.status = 'published'
    AND    v.created_at >= now() - INTERVAL '48 hours'
  LOOP
    PERFORM battles.fn_refresh_vote_velocity(r.id);
    refreshed := refreshed + 1;
  END LOOP;
  RETURN refreshed;
END;
$$;


ALTER FUNCTION "battles"."fn_refresh_all_vote_velocities"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_refresh_vote_velocity"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  v_published_at TIMESTAMPTZ;
  v_hours        NUMERIC;
  v_votes_24h    INT;
BEGIN
  SELECT published_at INTO v_published_at FROM battles.battles WHERE id = p_battle_id;
  IF v_published_at IS NULL THEN RETURN; END IF;

  v_hours := GREATEST(1, EXTRACT(EPOCH FROM (now() - v_published_at)) / 3600.0);

  SELECT COUNT(*) INTO v_votes_24h
  FROM   battles.votes
  WHERE  battle_id  = p_battle_id
  AND    created_at >= now() - INTERVAL '24 hours';

  UPDATE battles.battles
  SET    vote_velocity = ROUND((v_votes_24h / v_hours)::NUMERIC, 4)
  WHERE  id = p_battle_id;
END;
$$;


ALTER FUNCTION "battles"."fn_refresh_vote_velocity"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
BEGIN
  UPDATE battles.battle_execution_jobs
  SET    status        = 'queued',
         worker_id     = NULL,
         claimed_at    = now() + (p_backoff_ms || ' milliseconds')::INTERVAL,
         retry_count   = retry_count + 1,
         error_message = p_error
  WHERE  id = p_job_id;
END;
$$;


ALTER FUNCTION "battles"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
DECLARE
  v_dlq battles.battle_execution_dead_letters;
BEGIN
  SELECT * INTO v_dlq FROM battles.battle_execution_dead_letters WHERE id = p_dead_letter_id;
  IF NOT FOUND THEN RAISE EXCEPTION 'dead letter not found: %', p_dead_letter_id; END IF;
  IF v_dlq.resolved_at IS NOT NULL THEN RAISE EXCEPTION 'dead letter already resolved'; END IF;

  -- Requeue the original job
  UPDATE battles.battle_execution_jobs
  SET    status        = 'queued',
         worker_id     = NULL,
         claimed_at    = NULL,
         retry_count   = retry_count + 1,
         error_message = NULL
  WHERE  id = v_dlq.job_id;

  -- Mark DLQ entry as resolved
  UPDATE battles.battle_execution_dead_letters
  SET    resolved_at = now()
  WHERE  id = p_dead_letter_id;
END;
$$;


ALTER FUNCTION "battles"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_series_has_published_round"("p_series_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT EXISTS (
    SELECT 1
      FROM battles.series_rounds sr
      JOIN battles.battles b ON b.id = sr.battle_id
     WHERE sr.series_id = p_series_id
       AND b.status::text = 'published'
  );
$$;


ALTER FUNCTION "battles"."fn_series_has_published_round"("p_series_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_series_round_owner_id"("p_series_id" "uuid") RETURNS "uuid"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT s.creator_lenser_id
    FROM battles.series s
   WHERE s.id = p_series_id
   LIMIT 1;
$$;


ALTER FUNCTION "battles"."fn_series_round_owner_id"("p_series_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_series_touch_updated"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles', 'pg_catalog'
    AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_series_touch_updated"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_submit_agent_vote"("p_battle_id" "uuid", "p_voter_lenser_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean DEFAULT false, "p_rationale" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_vote_id UUID;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles WHERE id = p_battle_id AND status = 'voting'
  ) THEN
    RAISE EXCEPTION 'battle_not_in_voting_phase: %', p_battle_id;
  END IF;

  IF NOT p_is_draw AND NOT EXISTS (
    SELECT 1 FROM battles.contenders
    WHERE  id = p_voted_contender_id AND battle_id = p_battle_id
  ) THEN
    RAISE EXCEPTION 'contender_not_in_battle: %', p_voted_contender_id;
  END IF;

  INSERT INTO battles.votes
    (battle_id, voter_lenser_id, vote_value, voted_contender_id, is_draw, rationale)
  VALUES
    (p_battle_id, p_voter_lenser_id,
     p_vote_value::battles.vote_value_enum,
     p_voted_contender_id, p_is_draw, p_rationale)
  RETURNING id INTO v_vote_id;

  INSERT INTO battles.vote_aggregates
    (battle_id, contender_id, raw_vote_count, weighted_vote_sum, draw_count)
  VALUES
    (p_battle_id, p_voted_contender_id, 1, 1, CASE WHEN p_is_draw THEN 1 ELSE 0 END)
  ON CONFLICT (battle_id, contender_id) DO UPDATE SET
    raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
    weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
    draw_count        = battles.vote_aggregates.draw_count
                          + CASE WHEN p_is_draw THEN 1 ELSE 0 END,
    updated_at        = now();

  UPDATE battles.battles
  SET    total_vote_count = total_vote_count + 1
  WHERE  id = p_battle_id;

  INSERT INTO audit.events (event_type, payload)
  VALUES ('battle.agent_vote_submitted', jsonb_build_object(
    'vote_id',     v_vote_id,
    'battle_id',   p_battle_id,
    'voter_id',    p_voter_lenser_id,
    'vote_value',  p_vote_value
  ));

  RETURN jsonb_build_object(
    'vote_id',   v_vote_id,
    'status',    'success',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "battles"."fn_submit_agent_vote"("p_battle_id" "uuid", "p_voter_lenser_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_touch_comment_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_touch_comment_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_trg_notify_agent_battle_won"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_ai_lenser_id    UUID;
  v_ai_profile_id   UUID;
  v_owner_lenser_id UUID;
BEGIN
  -- Check if the winning contender is an AI lenser
  SELECT cem.ai_lenser_id
  INTO   v_ai_lenser_id
  FROM   battles.contender_entity_map cem
  WHERE  cem.contender_id   = NEW.winner_contender_id
    AND  cem.ai_lenser_id   IS NOT NULL;

  IF NOT FOUND OR v_ai_lenser_id IS NULL THEN
    RETURN NEW;  -- winner is a human contender, handled by fn_notify_battle_result
  END IF;

  -- Resolve the AI's own profile_id
  SELECT al.profile_id INTO v_ai_profile_id
  FROM   agents.ai_lensers al
  WHERE  al.id = v_ai_lenser_id;

  -- Find the human owner
  SELECT o.owner_lenser_id INTO v_owner_lenser_id
  FROM   agents.ownerships o
  WHERE  o.ai_lenser_id = v_ai_lenser_id
    AND  o.role         = 'owner'
    AND  o.revoked_at   IS NULL
  LIMIT  1;

  -- Notify the AI's profile (self-notification)
  IF v_ai_profile_id IS NOT NULL THEN
    PERFORM public.fn_insert_notification(
      v_ai_profile_id,
      'agent_battle_won',
      'You won the battle: ' || NEW.title,
      NULL,
      '/battles/' || NEW.slug || '/result',
      jsonb_build_object(
        'ai_lenser_id',  v_ai_lenser_id,
        'battle_id',     NEW.id,
        'battle_slug',   NEW.slug,
        'battle_title',  NEW.title
      )
    );
  END IF;

  -- Notify the human owner
  IF v_owner_lenser_id IS NOT NULL THEN
    PERFORM public.fn_insert_notification(
      v_owner_lenser_id,
      'agent_battle_won',
      'Your AI agent won the battle: ' || NEW.title,
      NULL,
      '/battles/' || NEW.slug || '/result',
      jsonb_build_object(
        'ai_lenser_id',  v_ai_lenser_id,
        'battle_id',     NEW.id,
        'battle_slug',   NEW.slug,
        'battle_title',  NEW.title
      )
    );
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_agent_battle_won failed for battle %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_trg_notify_agent_battle_won"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_trg_notify_battle_comment"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_creator_id  UUID;
  v_battle_slug TEXT;
  v_commenter   lensers.profiles%ROWTYPE;
  v_notif_id    UUID;
BEGIN
  SELECT b.creator_lenser_id, b.slug
  INTO   v_creator_id, v_battle_slug
  FROM   battles.battles b
  WHERE  b.id = NEW.battle_id;

  IF NOT FOUND OR v_creator_id = NEW.lenser_id THEN
    RETURN NEW;
  END IF;

  -- Anti-spam: 5-minute window per battle
  IF NOT public.fn_check_and_upsert_aggregate(
    v_creator_id, NEW.lenser_id, 'battle_comment', NEW.battle_id, '5 minutes'
  ) THEN
    RETURN NEW;
  END IF;

  SELECT * INTO v_commenter FROM lensers.profiles WHERE id = NEW.lenser_id;

  v_notif_id := public.fn_insert_notification(
    v_creator_id,
    'battle_comment',
    COALESCE(v_commenter.display_name, v_commenter.handle) || ' commented on your battle',
    NULL,
    '/battles/' || v_battle_slug,
    jsonb_build_object(
      'commenter_id',           NEW.lenser_id,
      'commenter_handle',       v_commenter.handle,
      'commenter_display_name', v_commenter.display_name,
      'battle_id',              NEW.battle_id,
      'battle_slug',            v_battle_slug,
      'preview',                LEFT(NEW.body, 120)
    ),
    NEW.lenser_id
  );

  IF v_notif_id IS NOT NULL THEN
    UPDATE public.notification_aggregates
    SET    notification_id = v_notif_id
    WHERE  recipient_id      = v_creator_id
      AND  notification_type = 'battle_comment'
      AND  entity_id         = NEW.battle_id
      AND  notification_id   IS NULL
      AND  window_end        > now();
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_battle_comment failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_trg_notify_battle_comment"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_trg_notify_battle_joined"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_battle_id       UUID;
  v_creator_id      UUID;
  v_battle_slug     TEXT;
  v_battle_title    TEXT;
  v_joiner          lensers.profiles%ROWTYPE;
BEGIN
  -- Resolve battle from the contender row
  SELECT c.battle_id, b.creator_lenser_id, b.slug, b.title
  INTO   v_battle_id, v_creator_id, v_battle_slug, v_battle_title
  FROM   battles.contenders c
  JOIN   battles.battles b ON b.id = c.battle_id
  WHERE  c.id = NEW.contender_id;

  IF NOT FOUND OR v_creator_id = NEW.profile_id THEN
    RETURN NEW;  -- battle not found or self-join
  END IF;

  SELECT * INTO v_joiner FROM lensers.profiles WHERE id = NEW.profile_id;

  PERFORM public.fn_insert_notification(
    v_creator_id,
    'battle_joined',
    COALESCE(v_joiner.display_name, v_joiner.handle) || ' joined your battle',
    v_battle_title,
    '/battles/' || v_battle_slug,
    jsonb_build_object(
      'battle_id',           v_battle_id,
      'battle_slug',         v_battle_slug,
      'contender_id',        NEW.contender_id,
      'joiner_id',           NEW.profile_id,
      'joiner_handle',       v_joiner.handle,
      'joiner_display_name', v_joiner.display_name
    ),
    NEW.profile_id  -- p_actor_id for block/mute gate
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_battle_joined failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_trg_notify_battle_joined"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_trg_notify_battle_started"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_participant RECORD;
BEGIN
  -- Collect all contender profile_ids: human (cem.profile_id) and AI (ai_lensers.profile_id)
  FOR v_participant IN
    SELECT COALESCE(cem.profile_id, al.profile_id) AS lenser_id
    FROM   battles.contenders c
    JOIN   battles.contender_entity_map cem ON cem.contender_id = c.id
    LEFT   JOIN agents.ai_lensers al ON al.id = cem.ai_lenser_id
    WHERE  c.battle_id = NEW.id
      AND  COALESCE(cem.profile_id, al.profile_id) IS NOT NULL
  LOOP
    PERFORM public.fn_insert_notification(
      v_participant.lenser_id,
      'battle_started',
      'Battle is now open: ' || NEW.title,
      'Submit your entry — voting begins soon.',
      '/battles/' || NEW.slug,
      jsonb_build_object(
        'battle_id',    NEW.id,
        'battle_slug',  NEW.slug,
        'battle_title', NEW.title
      )
      -- p_actor_id = NULL: system event, skip block/mute gate
    );
  END LOOP;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_battle_started failed for battle %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_trg_notify_battle_started"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_trg_template_battle_status_notify"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public', 'lensers'
    AS $_$
DECLARE
  v_type  TEXT;
  v_title TEXT;
  v_body  TEXT;
BEGIN
  IF NEW.template_id IS NULL THEN
    RETURN NEW;
  END IF;
  IF NEW.status::text NOT IN ('open', 'published') THEN
    RETURN NEW;
  END IF;
  IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
    RETURN NEW;
  END IF;
  IF NEW.creator_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  v_type  := 'template_battle_' || NEW.status::text;
  v_title := CASE NEW.status::text
               WHEN 'open'      THEN format($f$Your template battle "%s" is now open$f$, NEW.title)
               WHEN 'published' THEN format($f$Your template battle "%s" has been published$f$, NEW.title)
             END;
  v_body  := CASE NEW.status::text
               WHEN 'open'      THEN 'Contenders can now submit entries.'
               WHEN 'published' THEN 'Results are live.'
             END;

  PERFORM public.fn_insert_notification(
    NEW.creator_lenser_id,
    v_type,
    v_title,
    v_body,
    '/battles/' || COALESCE(NEW.slug, NEW.id::text),
    jsonb_build_object(
      'battle_id',   NEW.id,
      'template_id', NEW.template_id,
      'title',       NEW.title,
      'status',      NEW.status::text
    )
  );

  RETURN NEW;
END $_$;


ALTER FUNCTION "battles"."fn_trg_template_battle_status_notify"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_update_handicap_timestamp"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_update_handicap_timestamp"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_validate_comment_lenser"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers'
    AS $$
BEGIN
  IF NEW.lenser_id IS NOT NULL AND
     NOT EXISTS (SELECT 1 FROM lensers.profiles WHERE id = NEW.lenser_id) THEN
    RAISE EXCEPTION 'lenser_id % does not exist in lensers.profiles', NEW.lenser_id;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."fn_validate_comment_lenser"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."fn_votes_set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles', 'pg_catalog'
    AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END $$;


ALTER FUNCTION "battles"."fn_votes_set_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."log_battle_status_event"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles'
    AS $$
BEGIN
    IF OLD.status IS DISTINCT FROM NEW.status THEN
        INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
        VALUES (
            NEW.id,
            'status_change',
            NEW.creator_lenser_id,
            jsonb_build_object(
                'from', OLD.status::text,
                'to', NEW.status::text
            )
        );
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."log_battle_status_event"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."set_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_battle_execution_jobs_capture_agent_snapshot"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_ai_lenser_id UUID;
BEGIN
  IF NEW.redacted_agent_snapshot IS NOT NULL THEN
    RETURN NEW;
  END IF;

  SELECT ai_lenser_id INTO v_ai_lenser_id
  FROM battles.contender_entity_map
  WHERE contender_id = NEW.contender_id;

  IF v_ai_lenser_id IS NOT NULL THEN
    NEW.redacted_agent_snapshot := public.fn_redacted_agent_snapshot(v_ai_lenser_id);
    NEW.redacted_agent_snapshot_hash := public.fn_redacted_agent_snapshot_hash(v_ai_lenser_id);
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_battle_execution_jobs_capture_agent_snapshot"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_battle_start_requires_challenge"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    AS $$
BEGIN
  -- Only enforce when transitioning from draft/open to executing
  IF OLD.status IN ('draft', 'open') AND NEW.status = 'executing' THEN
    IF NEW.task_source = 'challenge'
       AND NEW.contender_structure IN ('human_vs_human', 'human_vs_ai') THEN
      IF NEW.generated_challenge_id IS NULL THEN
        RAISE EXCEPTION 'battle_missing_generated_challenge: cannot start challenge battle without a locked generated challenge';
      END IF;
      -- Verify the challenge is actually locked
      IF NOT EXISTS (
        SELECT 1 FROM battles.generated_challenges gc
        WHERE gc.id = NEW.generated_challenge_id AND gc.status = 'locked'
      ) THEN
        RAISE EXCEPTION 'battle_challenge_not_locked: generated challenge must be locked before battle starts';
      END IF;
    END IF;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_battle_start_requires_challenge"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_battle_structural_immutability"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_summary JSONB;
  v_has_evidence BOOLEAN;
BEGIN
  IF TG_OP <> 'UPDATE' THEN
    RETURN NEW;
  END IF;

  v_summary := public.fn_artifact_dependency_summary('battle', OLD.id);
  v_has_evidence := COALESCE((v_summary ->> 'total')::int, 0) > 0
                    OR OLD.status <> 'draft'::battles.battle_status_enum;

  IF v_has_evidence AND (
      NEW.title IS DISTINCT FROM OLD.title
      OR NEW.slug IS DISTINCT FROM OLD.slug
      OR NEW.task_prompt IS DISTINCT FROM OLD.task_prompt
      OR NEW.rubric_id IS DISTINCT FROM OLD.rubric_id
      OR NEW.max_contenders IS DISTINCT FROM OLD.max_contenders
      OR NEW.battle_type IS DISTINCT FROM OLD.battle_type
      OR NEW.contender_structure IS DISTINCT FROM OLD.contender_structure
      OR NEW.judging_mode IS DISTINCT FROM OLD.judging_mode
      OR NEW.task_source IS DISTINCT FROM OLD.task_source
      OR NEW.challenge_type IS DISTINCT FROM OLD.challenge_type
      OR NEW.generated_challenge_id IS DISTINCT FROM OLD.generated_challenge_id
      OR NEW.voter_eligibility IS DISTINCT FROM OLD.voter_eligibility
      OR NEW.handicap_config IS DISTINCT FROM OLD.handicap_config
      OR NEW.workflow_id IS DISTINCT FROM OLD.workflow_id
      OR NEW.lens_id IS DISTINCT FROM OLD.lens_id
      OR NEW.content_type IS DISTINCT FROM OLD.content_type
      OR NEW.ai_judge_enabled IS DISTINCT FROM OLD.ai_judge_enabled
      OR NEW.ai_judge_model_key IS DISTINCT FROM OLD.ai_judge_model_key
      OR NEW.ai_judge_prompt IS DISTINCT FROM OLD.ai_judge_prompt
    ) THEN
    RAISE EXCEPTION 'battle_config_immutable: battle has historical or competitive evidence'
      USING ERRCODE = '55000',
            DETAIL = v_summary::text;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_battle_structural_immutability"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_contender_entity_map_capture_agent_snapshot"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  IF NEW.ai_lenser_id IS NOT NULL AND NEW.redacted_agent_snapshot IS NULL THEN
    NEW.redacted_agent_snapshot := public.fn_redacted_agent_snapshot(NEW.ai_lenser_id);
    NEW.redacted_agent_snapshot_hash := public.fn_redacted_agent_snapshot_hash(NEW.ai_lenser_id);
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_contender_entity_map_capture_agent_snapshot"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_enforce_status_transition"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
    v_valid boolean;
BEGIN
    IF OLD.status = NEW.status THEN
        RETURN NEW;
    END IF;

    v_valid := CASE OLD.status::text
        WHEN 'draft'     THEN NEW.status::text IN ('open', 'archived')
        WHEN 'open'      THEN NEW.status::text IN ('executing', 'voting', 'archived')
        WHEN 'executing' THEN NEW.status::text IN ('voting', 'archived')
        WHEN 'voting'    THEN NEW.status::text IN ('scoring', 'archived')
        WHEN 'scoring'   THEN NEW.status::text IN ('closed')
        WHEN 'closed'    THEN NEW.status::text IN ('published')
        WHEN 'published' THEN NEW.status::text IN ('archived')
        WHEN 'archived'  THEN false
        ELSE false
    END;

    IF NOT v_valid THEN
        RAISE EXCEPTION 'INVALID_STATUS_TRANSITION: Cannot transition battle from % to % (battle_id=%)',
            OLD.status, NEW.status, OLD.id;
    END IF;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_enforce_status_transition"() OWNER TO "postgres";


COMMENT ON FUNCTION "battles"."trg_enforce_status_transition"() IS 'D11 fix: extends the legal transition map with executing state so fn_auto_start_battles (open→executing) and fn_complete_battle_execution_job (executing→voting) succeed. Also adds archived as a safety exit from open, executing, and voting for manual cancellation.';



CREATE OR REPLACE FUNCTION "battles"."trg_events_immutable"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles'
    AS $$
BEGIN
    IF TG_OP = 'DELETE' THEN
        RAISE EXCEPTION 'Battle events are immutable. Deletion is not permitted.';
    ELSIF TG_OP = 'UPDATE' THEN
        RAISE EXCEPTION 'Battle events are immutable. Updates are not permitted.';
    END IF;
    RETURN NULL;
END;
$$;


ALTER FUNCTION "battles"."trg_events_immutable"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_generated_challenge_immutable"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    AS $$
BEGIN
  IF OLD.status = 'locked' AND NEW.status IS DISTINCT FROM 'locked' THEN
    RAISE EXCEPTION 'generated_challenge_immutable: cannot modify a locked challenge';
  END IF;
  IF OLD.status = 'locked' THEN
    -- Allow no content changes once locked
    IF NEW.question_text IS DISTINCT FROM OLD.question_text
       OR NEW.question_payload IS DISTINCT FROM OLD.question_payload
       OR NEW.answer_key_encrypted IS DISTINCT FROM OLD.answer_key_encrypted
       OR NEW.content_hash IS DISTINCT FROM OLD.content_hash
       OR NEW.difficulty IS DISTINCT FROM OLD.difficulty
       OR NEW.language IS DISTINCT FROM OLD.language
       OR NEW.time_limit_seconds IS DISTINCT FROM OLD.time_limit_seconds THEN
      RAISE EXCEPTION 'generated_challenge_immutable: locked challenge content cannot be changed';
    END IF;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_generated_challenge_immutable"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_notify_ai_vote_cast"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_battle battles.battles%ROWTYPE;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = NEW.battle_id;

  PERFORM public.fn_insert_notification(
    NEW.voter_lenser_id,
    'battle_vote_cast',
    'Vote recorded: ' || COALESCE(v_battle.title, 'battle'),
    'Voted for contender ' || NEW.voted_contender_id::text,
    '/battles/' || v_battle.slug,
    jsonb_build_object(
      'battle_id',          NEW.battle_id,
      'battle_slug',        v_battle.slug,
      'voted_contender_id', NEW.voted_contender_id,
      'vote_value',         NEW.vote_value::text
    )
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_ai_vote_cast failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_notify_ai_vote_cast"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_notify_battle_assigned"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_ai_profile_id UUID;
  v_owner_id      UUID;
  v_battle        battles.battles%ROWTYPE;
  v_contender     battles.contenders%ROWTYPE;
BEGIN
  SELECT profile_id INTO v_ai_profile_id
  FROM   agents.ai_lensers
  WHERE  id = NEW.ai_lenser_id;

  SELECT owner_lenser_id INTO v_owner_id
  FROM   agents.ownerships
  WHERE  ai_lenser_id = NEW.ai_lenser_id
  AND    role = 'owner'
  AND    revoked_at IS NULL
  LIMIT  1;

  SELECT * INTO v_contender FROM battles.contenders  WHERE id = NEW.contender_id;
  SELECT * INTO v_battle    FROM battles.battles      WHERE id = v_contender.battle_id;

  -- Notify AI lenser profile
  IF v_ai_profile_id IS NOT NULL THEN
    PERFORM public.fn_insert_notification(
      v_ai_profile_id,
      'battle_assigned',
      'You have been assigned to a battle',
      v_battle.title,
      '/battles/' || v_battle.slug,
      jsonb_build_object(
        'battle_id',    v_battle.id,
        'battle_slug',  v_battle.slug,
        'battle_title', v_battle.title,
        'slot',         v_contender.slot,
        'contender_id', v_contender.id
      )
    );
  END IF;

  -- Notify human owner
  IF v_owner_id IS NOT NULL THEN
    PERFORM public.fn_insert_notification(
      v_owner_id,
      'agent_update',
      'Your AI lenser was assigned to a battle',
      v_battle.title,
      '/battles/' || v_battle.slug,
      jsonb_build_object(
        'ai_lenser_id',   NEW.ai_lenser_id,
        'ai_profile_id',  v_ai_profile_id,
        'battle_id',      v_battle.id,
        'battle_slug',    v_battle.slug,
        'event',          'battle_assigned'
      )
    );
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_battle_assigned failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_notify_battle_assigned"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_notify_vote_received"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_contender_owner_id UUID;
  v_voter              lensers.profiles%ROWTYPE;
  v_battle             battles.battles%ROWTYPE;
BEGIN
  -- Find the profile_id of the voted contender
  SELECT cem.profile_id INTO v_contender_owner_id
  FROM   battles.contender_entity_map cem
  WHERE  cem.contender_id = NEW.voted_contender_id
  AND    cem.profile_id IS NOT NULL;

  IF v_contender_owner_id IS NULL THEN
    RETURN NEW;  -- AI contender, handled by trg_notify_ai_vote_cast
  END IF;

  -- Skip self-votes
  IF v_contender_owner_id = NEW.voter_lenser_id THEN
    RETURN NEW;
  END IF;

  SELECT * INTO v_voter  FROM lensers.profiles WHERE id = NEW.voter_lenser_id;
  SELECT * INTO v_battle FROM battles.battles   WHERE id = NEW.battle_id;

  PERFORM public.fn_insert_notification(
    v_contender_owner_id,
    'vote_received',
    COALESCE(v_voter.display_name, v_voter.handle) || ' voted on your entry',
    v_battle.title,
    '/battles/' || v_battle.slug,
    jsonb_build_object(
      'battle_id',          NEW.battle_id,
      'battle_slug',        v_battle.slug,
      'voter_id',           NEW.voter_lenser_id,
      'voter_handle',       v_voter.handle,
      'vote_value',         NEW.vote_value::text,
      'voted_contender_id', NEW.voted_contender_id
    )
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_vote_received failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_notify_vote_received"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_prevent_self_vote"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
    -- Check if voter is a contender in this battle (by lenser_id only)
    IF EXISTS (
        SELECT 1 FROM battles.contenders c
        WHERE c.battle_id = NEW.battle_id
          AND c.contender_type = 'human'
          AND c.contender_ref_id = NEW.voter_lenser_id
    ) THEN
        RAISE EXCEPTION 'SELF_VOTE_BLOCKED: Contenders cannot vote on their own battle (voter_lenser_id=%, battle_id=%)',
            NEW.voter_lenser_id, NEW.battle_id;
    END IF;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_prevent_self_vote"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_refresh_vote_velocity"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'pg_catalog'
    AS $$
BEGIN
  PERFORM battles.fn_refresh_vote_velocity(NEW.battle_id);
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_refresh_vote_velocity"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_snapshot_battle_on_first_open"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  IF OLD.status = 'draft'::battles.battle_status_enum
     AND NEW.status <> 'draft'::battles.battle_status_enum
     AND NOT EXISTS (SELECT 1 FROM battles.rule_snapshots rs WHERE rs.battle_id = NEW.id) THEN
    PERFORM public.fn_create_battle_config_snapshot(NEW.id, 'status_transition');
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_snapshot_battle_on_first_open"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "battles"."trg_submissions_validate_execution_link"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'battles', 'execution'
    AS $$
DECLARE
    v_run RECORD;
BEGIN
    IF NEW.execution_run_id IS NULL THEN
        RETURN NEW;
    END IF;

    SELECT r.status, r.request_id, rq.requester_lenser_id
    INTO v_run
    FROM execution.runs r
    JOIN execution.requests rq ON rq.id = r.request_id
    WHERE r.id = NEW.execution_run_id;

    IF v_run IS NULL THEN
        RAISE EXCEPTION 'Execution run not found: %', NEW.execution_run_id;
    END IF;

    IF v_run.status <> 'succeeded' THEN
        RAISE EXCEPTION 'Execution run must have succeeded status (current: %)', v_run.status;
    END IF;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "battles"."trg_submissions_validate_execution_link"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "billing"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint DEFAULT 0, "p_output_tokens" bigint DEFAULT 0, "p_units" integer DEFAULT 1) RETURNS bigint
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'billing', 'ai', 'wallet'
    AS $_$
DECLARE
    v_raw_usd           numeric;
    v_markup_usd        numeric;
    v_credits           bigint;
    v_markup_percent    numeric;
    v_fixed_fee_usd     numeric;
    v_rounding_mode     text;
    v_min_credits       integer;
    v_max_credits       integer;
    v_unit_type         text;
    v_input_cost_1k     numeric;
    v_output_cost_1k    numeric;
    v_cost_per_unit     numeric;
    v_credits_per_usd   numeric;
BEGIN
    -- Read dynamic credit valuation rate
    SELECT cvp.baseline_credits_per_usd INTO v_credits_per_usd
    FROM wallet.credit_valuation_policy cvp
    WHERE cvp.singleton = true
    LIMIT 1;

    IF v_credits_per_usd IS NULL THEN
        v_credits_per_usd := 100;  -- safe default: 1 credit = $0.01
    END IF;

    -- Get most recent pricing row for model
    SELECT pt.unit_type, pt.input_cost_per_1k_tokens, pt.output_cost_per_1k_tokens, pt.cost_per_unit
    INTO v_unit_type, v_input_cost_1k, v_output_cost_1k, v_cost_per_unit
    FROM ai.model_pricing pt
    WHERE pt.model_id = p_model_id
        AND pt.effective_from <= now()
        AND (pt.effective_to IS NULL OR pt.effective_to > now())
    ORDER BY pt.effective_from DESC
    LIMIT 1;

    IF v_unit_type IS NULL THEN
        RAISE EXCEPTION 'PRICING_NOT_FOUND: no pricing available for model %', p_model_id;
    END IF;

    -- Calculate raw USD based on unit type
    IF v_unit_type = 'tokens' THEN
        v_raw_usd := (p_input_tokens::numeric / 1000 * v_input_cost_1k) +
                     (p_output_tokens::numeric / 1000 * v_output_cost_1k);
    ELSIF v_unit_type IN ('image', 'video_second', 'audio_second') THEN
        v_raw_usd := p_units::numeric * v_cost_per_unit;
    ELSE
        RAISE EXCEPTION 'INVALID_UNIT_TYPE: %', v_unit_type;
    END IF;

    -- Get margin policy: model-specific first, then global default (model_id IS NULL).
    -- FIX: added (emp.model_id = p_model_id OR emp.model_id IS NULL) so only
    -- applicable policies are considered. ORDER BY model_id NULLS LAST ensures
    -- model-specific rows win over the global default row.
    SELECT emp.markup_percent, emp.fixed_fee_usd, emp.rounding_mode,
           emp.min_charge_credits, emp.max_charge_credits
    INTO v_markup_percent, v_fixed_fee_usd, v_rounding_mode, v_min_credits, v_max_credits
    FROM billing.execution_margin_policies emp
    WHERE emp.is_active = true
        AND (emp.model_id = p_model_id OR emp.model_id IS NULL)
        AND emp.effective_from <= now()
        AND (emp.effective_to IS NULL OR emp.effective_to > now())
    ORDER BY emp.model_id NULLS LAST, emp.effective_from DESC
    LIMIT 1;

    IF v_markup_percent IS NULL THEN
        v_markup_percent := 0;
        v_fixed_fee_usd  := 0;
        v_rounding_mode  := 'ceil';
        v_min_credits    := 1;
        v_max_credits    := NULL;
    END IF;

    -- Apply markup
    v_markup_usd := v_raw_usd * (1 + v_markup_percent / 100) + v_fixed_fee_usd;

    -- Convert to credits using dynamic rate
    v_credits := v_markup_usd * v_credits_per_usd;

    -- Apply rounding
    IF v_rounding_mode = 'ceil' THEN
        v_credits := ceil(v_credits)::bigint;
    ELSIF v_rounding_mode = 'floor' THEN
        v_credits := floor(v_credits)::bigint;
    ELSIF v_rounding_mode = 'round' THEN
        v_credits := round(v_credits)::bigint;
    ELSE
        v_credits := ceil(v_credits)::bigint;
    END IF;

    -- Clamp to min/max
    IF v_min_credits IS NOT NULL THEN
        v_credits := GREATEST(v_credits, v_min_credits::bigint);
    END IF;
    IF v_max_credits IS NOT NULL THEN
        v_credits := LEAST(v_credits, v_max_credits::bigint);
    END IF;

    RETURN GREATEST(v_credits, 0);
END;
$_$;


ALTER FUNCTION "billing"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "billing"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) IS 'Pricing engine: convert raw model costs to credits with markup and rounding. Reads credit rate from wallet.credit_valuation_policy (default 100 credits/USD). Reads margin policy scoped to (model_id = p_model_id OR model_id IS NULL). Returns bigint credits. Raises: PRICING_NOT_FOUND, INVALID_UNIT_TYPE.';



CREATE OR REPLACE FUNCTION "billing"."tg_ledger_entries_hash"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_prev   bytea;
  v_canon  text;
BEGIN
  SELECT entry_hash INTO v_prev
  FROM   billing.ledger_entries
  WHERE  reservation_id = NEW.reservation_id
  ORDER  BY id DESC
  LIMIT  1;

  NEW.prev_hash := v_prev;  -- NULL for the first entry of the chain

  v_canon := jsonb_build_object(
    'reservation_id', NEW.reservation_id,
    'direction',      NEW.direction,
    'amount_credits', NEW.amount_credits::text,
    'amount_usd',     NEW.amount_usd::text,
    'recorded_at',    extract(epoch from NEW.recorded_at)::text
  )::text;

  NEW.entry_hash := extensions.digest(
    COALESCE(v_prev, ''::bytea) || convert_to(v_canon, 'UTF8'),
    'sha256'
  );

  RETURN NEW;
END;
$$;


ALTER FUNCTION "billing"."tg_ledger_entries_hash"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "billing"."tg_ledger_entries_immutable"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    AS $$
BEGIN
  RAISE EXCEPTION 'billing.ledger_entries is append-only (% blocked)', TG_OP
    USING ERRCODE = 'P0001';
END;
$$;


ALTER FUNCTION "billing"."tg_ledger_entries_immutable"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "connectors"."fn_active_workspace_id"() RETURNS "uuid"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'tenancy', 'lensers'
    AS $$
    SELECT w.id
    FROM tenancy.workspaces w
    WHERE w.owner_lenser_id = lensers.get_auth_lenser_id()
      AND w.type = 'personal'
      AND w.status = 'active'
    ORDER BY w.created_at ASC
    LIMIT 1
$$;


ALTER FUNCTION "connectors"."fn_active_workspace_id"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "connectors"."fn_assert_known_scopes"("p_scopes" "text"[]) RETURNS "void"
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'connectors', 'pg_catalog'
    AS $$
DECLARE
    v_unknown text[];
BEGIN
    SELECT array_agg(s)
      INTO v_unknown
      FROM unnest(p_scopes) s
     WHERE s <> ALL ("connectors"."fn_valid_scopes"());
    IF v_unknown IS NOT NULL THEN
        RAISE EXCEPTION 'unknown scopes: %', array_to_string(v_unknown, ',')
              USING ERRCODE = '22023';
    END IF;
END
$$;


ALTER FUNCTION "connectors"."fn_assert_known_scopes"("p_scopes" "text"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "connectors"."fn_assert_scope"("p_granted" "text"[], "p_required" "text") RETURNS "void"
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'connectors', 'pg_catalog'
    AS $$
BEGIN
    IF p_required IS NULL OR p_required = '' THEN
        RETURN;
    END IF;
    IF p_granted IS NULL OR p_required <> ALL (p_granted) THEN
        RAISE EXCEPTION 'connector token missing required scope: %', p_required
              USING ERRCODE = '42501';
    END IF;
END
$$;


ALTER FUNCTION "connectors"."fn_assert_scope"("p_granted" "text"[], "p_required" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "connectors"."fn_hash_token"("p_token" "text") RETURNS "text"
    LANGUAGE "sql" IMMUTABLE
    SET "search_path" TO 'connectors', 'pg_catalog'
    AS $$
    SELECT encode(extensions.digest(p_token, 'sha256'), 'hex')
$$;


ALTER FUNCTION "connectors"."fn_hash_token"("p_token" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "connectors"."fn_valid_scopes"() RETURNS "text"[]
    LANGUAGE "sql" IMMUTABLE
    SET "search_path" TO 'connectors', 'pg_catalog'
    AS $$
    SELECT ARRAY[
        'lenses:read', 'lenses:write',
        'agents:read', 'agents:write',
        'workflows:read', 'workflows:write',
        'threads:read', 'threads:write',
        'community:read', 'community:write',
        'connectors:read', 'connectors:write'
    ]::text[]
$$;


ALTER FUNCTION "connectors"."fn_valid_scopes"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."ensure_public_tag"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$BEGIN
  PERFORM 1 FROM content.tags t
  WHERE t.id = NEW.tag_id AND t.visibility = 'public';
  IF NOT FOUND THEN
    RAISE EXCEPTION 'Only public tags can be attached.';
  END IF;
  RETURN NEW;
END;$$;


ALTER FUNCTION "content"."ensure_public_tag"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."fn_check_reaction_target"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO ''
    AS $$
DECLARE
  ok boolean;
BEGIN
  IF NEW.entity_type = 'lens' THEN
    SELECT EXISTS(SELECT 1 FROM lenses.lenses WHERE id = NEW.entity_id) INTO ok;
  ELSIF NEW.entity_type = 'thread' THEN
    SELECT EXISTS(SELECT 1 FROM content.threads WHERE id = NEW.entity_id) INTO ok;
  ELSIF NEW.entity_type = 'thread_reply' THEN
    SELECT EXISTS(SELECT 1 FROM content.thread_replies WHERE id = NEW.entity_id) INTO ok;
  ELSIF NEW.entity_type = 'workflow' THEN
    SELECT EXISTS(SELECT 1 FROM lenses.workflows WHERE id = NEW.entity_id) INTO ok;
  ELSE
    RAISE EXCEPTION 'unsupported reaction entity_type: %', NEW.entity_type
      USING ERRCODE = 'check_violation';
  END IF;

  IF NOT ok THEN
    RAISE EXCEPTION 'reaction target %/% does not exist', NEW.entity_type, NEW.entity_id
      USING ERRCODE = 'foreign_key_violation';
  END IF;

  RETURN NEW;
END $$;


ALTER FUNCTION "content"."fn_check_reaction_target"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."fn_cleanup_entity_refs"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  DELETE FROM "content"."reactions"
    WHERE "entity_type" = "p_entity_type"
      AND "entity_id"   = "p_entity_id";

  DELETE FROM "content"."tag_map"
    WHERE "entity_type" = "p_entity_type"
      AND "entity_id"   = "p_entity_id";

  DELETE FROM "content"."entity_translations"
    WHERE "entity_type" = "p_entity_type"
      AND "entity_id"   = "p_entity_id";

  DELETE FROM "content"."tag_suggestions"
    WHERE "entity_type" = "p_entity_type"
      AND "entity_id"   = "p_entity_id";

  DELETE FROM "content"."reports"
    WHERE "target_type" = "p_entity_type"
      AND "target_id"   = "p_entity_id";
$$;


ALTER FUNCTION "content"."fn_cleanup_entity_refs"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "content"."fn_cleanup_entity_refs"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid") IS 'Deletes all polymorphic content references for a given entity when it is deleted. Called by AFTER DELETE triggers on lenses.lenses, content.threads, and content.thread_replies. Enforces referential integrity for entity_type + entity_id patterns that cannot use FK constraints.';



CREATE OR REPLACE FUNCTION "content"."fn_trg_notify_lens_comment"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_linked_lens_id         UUID;
  v_lens_owner_id          UUID;
  v_lens_vis               content.visibility_enum;
  v_commenter_display_name TEXT;
  v_commenter_handle       TEXT;
  v_lens_title             TEXT;
  v_notif_id               UUID;
BEGIN
  SELECT t.linked_lens_id
    INTO v_linked_lens_id
    FROM content.threads t
   WHERE t.id = NEW.thread_id;

  IF v_linked_lens_id IS NULL THEN
    RETURN NEW;
  END IF;

  SELECT l.lenser_id, l.visibility
    INTO v_lens_owner_id, v_lens_vis
    FROM lenses.lenses l
   WHERE l.id = v_linked_lens_id;

  IF NOT FOUND OR v_lens_vis = 'private'::content.visibility_enum THEN
    RETURN NEW;
  END IF;

  IF NOT public.fn_check_and_upsert_aggregate(
    v_lens_owner_id, NEW.lenser_id, 'lens_comment', v_linked_lens_id, '5 minutes'
  ) THEN
    RETURN NEW;
  END IF;

  SELECT display_name, handle
    INTO v_commenter_display_name, v_commenter_handle
    FROM lensers.profiles
   WHERE id = NEW.lenser_id;

  SELECT et.title
    INTO v_lens_title
    FROM content.entity_translations et
   WHERE et.entity_id   = v_linked_lens_id
     AND et.entity_type = 'lens'::content.entity_type_enum
     AND et.is_original = true
   LIMIT 1;

  v_notif_id := public.fn_insert_notification(
    v_lens_owner_id,
    'lens_comment',
    COALESCE(v_commenter_display_name, v_commenter_handle) || ' commented on your lens',
    COALESCE(v_lens_title, 'Your lens'),
    '/lenses/' || v_linked_lens_id,
    jsonb_build_object(
      'commenter_id',           NEW.lenser_id,
      'commenter_handle',       v_commenter_handle,
      'commenter_display_name', v_commenter_display_name,
      'entity_id',              v_linked_lens_id,
      'entity_type',            'lens',
      'preview',                LEFT(NEW.content, 120)
    ),
    NEW.lenser_id
  );

  IF v_notif_id IS NOT NULL THEN
    UPDATE public.notification_aggregates
       SET notification_id = v_notif_id
     WHERE recipient_id      = v_lens_owner_id
       AND notification_type = 'lens_comment'
       AND entity_id         = v_linked_lens_id
       AND notification_id   IS NULL
       AND window_end        > now();
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_lens_comment failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."fn_trg_notify_lens_comment"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."fn_xp_on_workflow_reaction"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'lenses', 'xp', 'public'
    AS $$
DECLARE
  v_owner_id   uuid;
  v_forum_app  uuid;
BEGIN
  -- Only process INSERT of workflow reactions (trigger WHEN clause already filters entity_type)
  IF TG_OP != 'INSERT' THEN RETURN NEW; END IF;

  -- Resolve forum app_id (cached as a constant; global fallback if not seeded)
  SELECT id INTO v_forum_app FROM xp.apps WHERE slug = 'forum' LIMIT 1;
  IF v_forum_app IS NULL THEN
    v_forum_app := '00000000-0000-0000-0000-000000000000'::uuid;
  END IF;

  -- Look up the workflow owner
  SELECT lenser_id INTO v_owner_id
  FROM lenses.workflows
  WHERE id = NEW.entity_id;

  IF NEW.reaction = 'like'::content.reaction_enum THEN
    -- Award XP to the liker
    BEGIN
      PERFORM xp.apply(NEW.lenser_id, 'WORKFLOW_LIKED', 'content'::xp.source_enum, 'workflow', NEW.entity_id, v_forum_app);
    EXCEPTION WHEN OTHERS THEN NULL; END;

    -- Award XP to the workflow owner (skip self-reactions)
    IF v_owner_id IS NOT NULL AND v_owner_id != NEW.lenser_id THEN
      BEGIN
        PERFORM xp.apply(v_owner_id, 'WORKFLOW_LIKE_RECEIVED', 'content'::xp.source_enum, 'workflow', NEW.entity_id, v_forum_app);
      EXCEPTION WHEN OTHERS THEN NULL; END;
    END IF;

  ELSIF NEW.reaction = 'saved'::content.reaction_enum THEN
    BEGIN
      PERFORM xp.apply(NEW.lenser_id, 'WORKFLOW_SAVED', 'content'::xp.source_enum, 'workflow', NEW.entity_id, v_forum_app);
    EXCEPTION WHEN OTHERS THEN NULL; END;

    IF v_owner_id IS NOT NULL AND v_owner_id != NEW.lenser_id THEN
      BEGIN
        PERFORM xp.apply(v_owner_id, 'WORKFLOW_SAVE_RECEIVED', 'content'::xp.source_enum, 'workflow', NEW.entity_id, v_forum_app);
      EXCEPTION WHEN OTHERS THEN NULL; END;
    END IF;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."fn_xp_on_workflow_reaction"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."normalize_website_url"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'content', 'public'
    AS $$
BEGIN
  IF NEW.website_url = '' THEN
    NEW.website_url := NULL;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."normalize_website_url"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."set_reaction_user_id"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'auth'
    AS $$
BEGIN
  NEW.user_id := auth.uid();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."set_reaction_user_id"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'content', 'public'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."set_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."set_version_published_at"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'public'
    AS $$
BEGIN
    IF NEW.status = 'published' AND OLD.status <> 'published' THEN
        NEW.published_at := now();
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."set_version_published_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."sync_lens_count"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'analytics'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF TG_OP = 'INSERT' THEN
    v_lenser_id := NEW.lenser_id;
  ELSIF TG_OP = 'DELETE' THEN
    v_lenser_id := OLD.lenser_id;
  ELSE
    RETURN NULL;
  END IF;

  UPDATE analytics.lenser_stats e
  SET lens_count = (
        SELECT COUNT(*)::int
        FROM lenses.lenses p
        WHERE p.lenser_id = v_lenser_id
      ),
      updated_at = now()
  WHERE e.lenser_id = v_lenser_id;

  RETURN NULL;
END;
$$;


ALTER FUNCTION "content"."sync_lens_count"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."sync_thread_count"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$DECLARE
  v_lenser_id uuid;
BEGIN
  IF (TG_OP = 'INSERT') THEN
    v_lenser_id := NEW.lenser_id;
  ELSIF (TG_OP = 'DELETE') THEN
    v_lenser_id := OLD.lenser_id;
  ELSE
    RETURN NULL;
  END IF;

  UPDATE analytics.lenser_stats e
  SET thread_count = (
        SELECT COUNT(*)::int
        FROM content.threads t
        WHERE t.lenser_id = v_lenser_id
      ),
      updated_at = now()
  WHERE e.lenser_id = v_lenser_id;

  RETURN NULL;
END;$$;


ALTER FUNCTION "content"."sync_thread_count"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."thread_replies_after_delete_trigger"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$begin
  if content.thread_reply_counts_as_public(OLD) then
    update content.threads
    set reply_count = greatest(reply_count - 1, 0)
    where id = OLD.thread_id;
  end if;

  return OLD;
end;$$;


ALTER FUNCTION "content"."thread_replies_after_delete_trigger"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."thread_replies_after_insert_trigger"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$begin
  if content.thread_reply_counts_as_public(NEW) then
    update content.threads
    set reply_count = reply_count + 1
    where id = NEW.thread_id;
  end if;

  return NEW;
end;$$;


ALTER FUNCTION "content"."thread_replies_after_insert_trigger"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."thread_replies_after_update_trigger"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$declare
  old_counts boolean;
  new_counts boolean;
begin
  old_counts := content.thread_reply_counts_as_public(OLD);
  new_counts := content.thread_reply_counts_as_public(NEW);

  -- became visible public reply
  if not old_counts and new_counts then
    update content.threads
    set reply_count = reply_count + 1
    where id = NEW.thread_id;

  -- stopped being counted (hidden / soft-deleted etc.)
  elsif old_counts and not new_counts then
    update content.threads
    set reply_count = greatest(reply_count - 1, 0)
    where id = OLD.thread_id;
  end if;

  return NEW;
end;$$;


ALTER FUNCTION "content"."thread_replies_after_update_trigger"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."get_auth_lenser_id"() RETURNS "uuid"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
  SELECT COALESCE(
    -- 1. Use stored active selection when it is still a valid, active profile
    (
      SELECT pref.active_lenser_id
      FROM lensers.preferences  pref
      JOIN lensers.profiles     human_p ON human_p.id = pref.lenser_id
      WHERE human_p.user_id           = auth.uid()
        AND pref.active_lenser_id     IS NOT NULL
        AND EXISTS (
          SELECT 1 FROM lensers.profiles target
          WHERE target.id     = pref.active_lenser_id
            AND target.status = 'active'
        )
      LIMIT 1
    ),
    -- 2. Fall back to the human profile
    (
      SELECT id FROM lensers.profiles
      WHERE user_id = auth.uid()
        AND type    = 'human'
      LIMIT 1
    )
  );
$$;


ALTER FUNCTION "lensers"."get_auth_lenser_id"() OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "content"."thread_replies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "thread_id" "uuid" NOT NULL,
    "parent_reply_id" "uuid",
    "lenser_id" "uuid" DEFAULT "lensers"."get_auth_lenser_id"() NOT NULL,
    "content" "text" NOT NULL,
    "content_html" "text",
    "status" "content"."thread_reply_status" DEFAULT 'published'::"content"."thread_reply_status" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"(),
    "updated_at" timestamp with time zone DEFAULT "now"(),
    "deleted_at" timestamp with time zone,
    CONSTRAINT "ck_content_thread_replies_content_len" CHECK ((("content" IS NULL) OR ("length"("content") <= 5000)))
);


ALTER TABLE "content"."thread_replies" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."thread_reply_counts_as_public"("r" "content"."thread_replies") RETURNS boolean
    LANGUAGE "sql"
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$
    SELECT (r.status = 'published'::content.thread_reply_status) AND r.deleted_at IS NULL;
  $$;


ALTER FUNCTION "content"."thread_reply_counts_as_public"("r" "content"."thread_replies") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."toggle_reaction"("p_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$
DECLARE
    v_entity    "content"."entity_type_enum";
    v_existing_id "uuid";
BEGIN
    v_entity := "p_target_type"::"content"."entity_type_enum";

    -- copy = unlimited → direct insert
    IF "p_reaction" = 'copy'::"content"."reaction_enum" THEN
        INSERT INTO "content"."reactions" ("entity_type", "entity_id", "lenser_id", "reaction")
        VALUES (v_entity, "p_target_id", "p_lenser_id", "p_reaction");
        RETURN "jsonb_build_object"('status', 'copied');
    END IF;

    -- Check existing toggleable reaction
    SELECT "id" INTO v_existing_id
    FROM "content"."reactions"
    WHERE "lenser_id"   = "p_lenser_id"
      AND "entity_type" = v_entity
      AND "entity_id"   = "p_target_id"
      AND "reaction"    IN ('like', 'dislike', 'saved');

    -- If exists → DELETE (toggle off)
    IF v_existing_id IS NOT NULL THEN
        DELETE FROM "content"."reactions" WHERE "id" = v_existing_id;
        RETURN "jsonb_build_object"('status', 'removed');
    END IF;

    -- Else → INSERT (toggle on)
    INSERT INTO "content"."reactions" ("entity_type", "entity_id", "lenser_id", "reaction")
    VALUES (v_entity, "p_target_id", "p_lenser_id", "p_reaction");

    RETURN "jsonb_build_object"('status', 'added');
END;
$$;


ALTER FUNCTION "content"."toggle_reaction"("p_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."trg_cleanup_thread_content_refs"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
  PERFORM "content"."fn_cleanup_entity_refs"('thread'::"content"."entity_type_enum", OLD."id");
  RETURN OLD;
END;
$$;


ALTER FUNCTION "content"."trg_cleanup_thread_content_refs"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."trg_cleanup_thread_reply_content_refs"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
  PERFORM "content"."fn_cleanup_entity_refs"('thread_reply'::"content"."entity_type_enum", OLD."id");
  RETURN OLD;
END;
$$;


ALTER FUNCTION "content"."trg_cleanup_thread_reply_content_refs"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."trg_notify_lens_reaction"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_owner_id UUID;
  v_reactor  lensers.profiles%ROWTYPE;
  v_slug     TEXT;
BEGIN
  -- Resolve entity owner (only lens and workflow supported)
  IF NEW.entity_type = 'lens' THEN
    SELECT lenser_id INTO v_owner_id FROM lenses.lenses WHERE id = NEW.entity_id;
    v_slug := '/lenses/' || NEW.entity_id;
  ELSIF NEW.entity_type = 'workflow' THEN
    SELECT lenser_id INTO v_owner_id FROM lenses.workflows WHERE id = NEW.entity_id;
    v_slug := '/workflows/' || NEW.entity_id;
  ELSE
    RETURN NEW;
  END IF;

  -- Skip if owner not found or this is a self-reaction
  IF v_owner_id IS NULL OR v_owner_id = NEW.lenser_id THEN
    RETURN NEW;
  END IF;

  SELECT * INTO v_reactor FROM lensers.profiles WHERE id = NEW.lenser_id;

  PERFORM public.fn_insert_notification(
    v_owner_id,
    'lens_reaction',
    COALESCE(v_reactor.display_name, v_reactor.handle) || ' reacted to your ' || NEW.entity_type::text,
    NULL,
    v_slug,
    jsonb_build_object(
      'reactor_id',           NEW.lenser_id,
      'reactor_handle',       v_reactor.handle,
      'reactor_display_name', v_reactor.display_name,
      'reactor_avatar_url',   v_reactor.avatar_url,
      'reaction',             NEW.reaction::text,
      'entity_type',          NEW.entity_type::text,
      'entity_id',            NEW.entity_id
    )
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_lens_reaction failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."trg_notify_lens_reaction"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."update_lens_reaction_counters"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses', 'content', 'public'
    AS $$
DECLARE
  v_template_id uuid;
BEGIN
  IF NEW.entity_type = 'lens' THEN
    v_template_id := NEW.target_id;

    UPDATE lenses.lenses
    SET reaction_totals = (
      SELECT jsonb_object_agg(reaction, count)
      FROM (
        SELECT reaction, COUNT(*) AS count
        FROM content.reactions
        WHERE target_type = 'lens'
          AND target_id   = v_template_id
        GROUP BY reaction
      ) sub
    )
    WHERE id = v_template_id;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."update_lens_reaction_counters"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."update_workflow_reaction_counters"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses', 'content', 'public'
    AS $$
DECLARE
  v_workflow_id uuid;
BEGIN
  -- Determine which workflow was affected (works for INSERT and DELETE)
  IF TG_OP = 'DELETE' THEN
    IF OLD.entity_type::text != 'workflow' THEN RETURN OLD; END IF;
    v_workflow_id := OLD.entity_id;
  ELSE
    IF NEW.entity_type::text != 'workflow' THEN RETURN NEW; END IF;
    v_workflow_id := NEW.entity_id;
  END IF;

  UPDATE lenses.workflows
  SET reaction_totals = COALESCE((
    SELECT jsonb_object_agg(reaction, cnt)
    FROM (
      SELECT reaction, COUNT(*) AS cnt
      FROM content.reactions
      WHERE entity_type = 'workflow'::content.entity_type_enum
        AND entity_id   = v_workflow_id
      GROUP BY reaction
    ) sub
  ), '{}'::jsonb)
  WHERE id = v_workflow_id;

  IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "content"."update_workflow_reaction_counters"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."user_owns_lens"("template_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public', 'auth'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM lenses.lenses pt
    JOIN lensers.profiles l ON l.id = pt.lenser_id
    WHERE pt.id = template_id
      AND l.user_id = auth.uid()
  );
$$;


ALTER FUNCTION "content"."user_owns_lens"("template_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "content"."user_owns_thread"("thread_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'content', 'lensers', 'public', 'auth'
    AS $$select exists (
    select 1
    from content.threads t
    join lensers.profiles l on l.id = t.lenser_id
    where t.id = thread_id
      and l.user_id = auth.uid()
  );$$;


ALTER FUNCTION "content"."user_owns_thread"("thread_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_acquire_leader_lease"("p_lease_kind" "text", "p_device_id" "uuid", "p_lease_seconds" integer DEFAULT 30) RETURNS TABLE("holder_device_id" "uuid", "holder_acquired" boolean, "expires_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_now       TIMESTAMPTZ := now();
  v_holder    UUID;
  v_expires   TIMESTAMPTZ;
  v_acquired  BOOLEAN := false;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM devices.registered_devices
    WHERE id = p_device_id
      AND lenser_id = v_lenser_id
      AND trust_level IN ('approved','trusted')
  ) THEN
    RAISE EXCEPTION 'device_not_trusted' USING ERRCODE = '42501';
  END IF;

  p_lease_seconds := GREATEST(LEAST(COALESCE(p_lease_seconds, 30), 600), 5);
  v_expires := v_now + make_interval(secs => p_lease_seconds);

  INSERT INTO devices.peer_leases (lease_kind, lenser_id, device_id, acquired_at, expires_at)
  VALUES (p_lease_kind, v_lenser_id, p_device_id, v_now, v_expires)
  ON CONFLICT (lease_kind, lenser_id) DO UPDATE
    SET device_id   = CASE
                        WHEN devices.peer_leases.expires_at < v_now
                          OR devices.peer_leases.device_id = EXCLUDED.device_id
                        THEN EXCLUDED.device_id
                        ELSE devices.peer_leases.device_id
                      END,
        acquired_at = CASE
                        WHEN devices.peer_leases.expires_at < v_now
                          OR devices.peer_leases.device_id = EXCLUDED.device_id
                        THEN v_now
                        ELSE devices.peer_leases.acquired_at
                      END,
        expires_at  = CASE
                        WHEN devices.peer_leases.expires_at < v_now
                          OR devices.peer_leases.device_id = EXCLUDED.device_id
                        THEN v_expires
                        ELSE devices.peer_leases.expires_at
                      END
  RETURNING device_id, expires_at INTO v_holder, v_expires;

  v_acquired := (v_holder = p_device_id);

  RETURN QUERY SELECT v_holder, v_acquired, v_expires;
END;
$$;


ALTER FUNCTION "devices"."fn_acquire_leader_lease"("p_lease_kind" "text", "p_device_id" "uuid", "p_lease_seconds" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "devices"."fn_acquire_leader_lease"("p_lease_kind" "text", "p_device_id" "uuid", "p_lease_seconds" integer) IS 'Acquire or renew a per-Lenser gateway leader lease for a trusted device. The current holder may renew before expiry; other devices may acquire only after expiry.';



CREATE OR REPLACE FUNCTION "devices"."fn_chain_on_device_registered"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'audit', 'public'
    AS $$
BEGIN
  PERFORM audit.fn_append_gateway_event_safe(
    'GATEWAY_DEVICE_REGISTERED',
    NEW.lenser_id,
    NEW.id,
    jsonb_build_object(
      'name',          NEW.name,
      'device_type',   NEW.device_type,
      'os',            NEW.os,
      'cli_version',   NEW.cli_version
    )
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "devices"."fn_chain_on_device_registered"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_chain_on_device_trust_changed"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'audit', 'public'
    AS $$
BEGIN
  IF NEW.trust_level IS DISTINCT FROM OLD.trust_level THEN
    PERFORM audit.fn_append_gateway_event_safe(
      'GATEWAY_DEVICE_TRUST_CHANGED',
      NEW.lenser_id,
      NEW.id,
      jsonb_build_object(
        'from', OLD.trust_level,
        'to',   NEW.trust_level
      )
    );
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "devices"."fn_chain_on_device_trust_changed"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_device_approve"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_device    devices.registered_devices%ROWTYPE;
  v_challenge devices.device_challenges%ROWTYPE;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_device
  FROM devices.registered_devices
  WHERE id = p_device_id
    AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'device_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF v_device.trust_level <> 'pending' THEN
    RAISE EXCEPTION 'device_not_pending' USING ERRCODE = 'P0001';
  END IF;

  -- Two-step path: device has a public_key, require an answered challenge.
  IF v_device.public_key IS NOT NULL THEN
    SELECT * INTO v_challenge
    FROM devices.device_challenges
    WHERE device_id = p_device_id
      AND lenser_id = v_lenser_id
      AND status    = 'answered'
      AND expires_at > now()
    ORDER BY created_at DESC
    LIMIT 1;

    IF v_challenge.id IS NULL THEN
      RAISE EXCEPTION 'awaiting_device_challenge' USING ERRCODE = 'P0001';
    END IF;

    UPDATE devices.device_challenges
       SET status = 'approved'
     WHERE id = v_challenge.id;
  END IF;

  UPDATE devices.registered_devices
     SET trust_level = 'approved'
   WHERE id = p_device_id;
END;
$$;


ALTER FUNCTION "devices"."fn_device_approve"("p_device_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "devices"."fn_device_approve"("p_device_id" "uuid") IS 'Two-step approve: devices with a recorded public_key require an answered challenge from devices.device_challenges. Legacy devices (no public_key) continue to work with single-step approve for one release window.';



CREATE OR REPLACE FUNCTION "devices"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text" DEFAULT NULL::"text", "p_envelope_sig" "text" DEFAULT NULL::"text", "p_gateway_status" "text" DEFAULT 'connected'::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  UPDATE devices.registered_devices
     SET last_heartbeat_at = now(),
         last_seen_at      = now(),
         daemon_version    = COALESCE(p_daemon_version, daemon_version),
         gateway_status    = COALESCE(p_gateway_status, gateway_status)
   WHERE id = p_device_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'device_not_found' USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "devices"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_device_list"("p_trust_level" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50, "p_cursor" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "name" "text", "device_type" "text", "os" "text", "arch" "text", "cli_version" "text", "runner_version" "text", "capabilities" "jsonb", "trust_level" "text", "gateway_status" "text", "last_seen_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required';
  END IF;

  p_limit := LEAST(p_limit, 100);

  RETURN QUERY
  SELECT
    d.id, d.name, d.device_type, d.os, d.arch,
    d.cli_version, d.runner_version, d.capabilities,
    d.trust_level, d.gateway_status, d.last_seen_at, d.created_at
  FROM devices.registered_devices d
  WHERE d.lenser_id = v_lenser_id
    AND (p_trust_level IS NULL OR d.trust_level = p_trust_level)
    AND (p_cursor IS NULL OR d.id > p_cursor)
  ORDER BY d.created_at DESC
  LIMIT p_limit;
END;
$$;


ALTER FUNCTION "devices"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone DEFAULT "now"()) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_challenge devices.device_challenges%ROWTYPE;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_challenge
  FROM devices.device_challenges
  WHERE device_id = p_device_id
    AND lenser_id = v_lenser_id
    AND status    = 'pending'
    AND expires_at > now()
  ORDER BY created_at DESC
  LIMIT 1;

  IF v_challenge.id IS NULL THEN
    RAISE EXCEPTION 'no_pending_challenge' USING ERRCODE = 'P0002';
  END IF;

  UPDATE devices.device_challenges
     SET status     = 'answered',
         signature  = p_signature,
         signed_iat = p_signed_iat
   WHERE id = v_challenge.id;

  RETURN v_challenge.id;
END;
$$;


ALTER FUNCTION "devices"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_device_register"("p_name" "text", "p_device_type" "text" DEFAULT 'other'::"text", "p_os" "text" DEFAULT NULL::"text", "p_arch" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_capabilities" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_device_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required';
  END IF;

  INSERT INTO devices.registered_devices (
    lenser_id, name, device_type, os, arch, cli_version, capabilities
  ) VALUES (
    v_lenser_id, p_name, p_device_type, p_os, p_arch, p_cli_version, p_capabilities
  )
  RETURNING id INTO v_device_id;

  RETURN v_device_id;
END;
$$;


ALTER FUNCTION "devices"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text" DEFAULT 'other'::"text", "p_os" "text" DEFAULT NULL::"text", "p_arch" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_daemon_version" "text" DEFAULT NULL::"text", "p_capabilities" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("device_id" "uuid", "challenge_id" "uuid", "challenge_nonce" "text", "challenge_expires_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_device_id UUID;
  v_challenge_id UUID;
  v_nonce TEXT;
  v_expires TIMESTAMPTZ;
BEGIN
  IF p_public_key IS NULL OR char_length(p_public_key) < 32 THEN
    RAISE EXCEPTION 'public_key_required' USING ERRCODE = '22023';
  END IF;

  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  INSERT INTO devices.registered_devices (
    lenser_id, name, device_type, os, arch, cli_version,
    capabilities, public_key, signing_algo, daemon_version
  ) VALUES (
    v_lenser_id, p_name, p_device_type, p_os, p_arch, p_cli_version,
    p_capabilities, p_public_key, 'ed25519', p_daemon_version
  )
  RETURNING id INTO v_device_id;

  v_nonce   := encode(extensions.gen_random_bytes(16), 'base64');
  v_expires := now() + interval '24 hours';

  INSERT INTO devices.device_challenges (device_id, lenser_id, nonce, expires_at)
  VALUES (v_device_id, v_lenser_id, v_nonce, v_expires)
  RETURNING id INTO v_challenge_id;

  RETURN QUERY SELECT v_device_id, v_challenge_id, v_nonce, v_expires;
END;
$$;


ALTER FUNCTION "devices"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_device_revoke"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  UPDATE devices.registered_devices
  SET trust_level = 'revoked', revoked_at = now()
  WHERE id = p_device_id
    AND lenser_id = v_lenser_id
    AND trust_level NOT IN ('revoked','blocked');

  IF NOT FOUND THEN
    RAISE EXCEPTION 'device_not_found_or_already_revoked';
  END IF;
END;
$$;


ALTER FUNCTION "devices"."fn_device_revoke"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "object_class" "text", "object_id" "text", "op" "text", "payload" "jsonb", "vclock" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_device_id UUID;
  v_kid       TEXT;
  v_nonce     TEXT;
  v_iat       BIGINT;
  v_class     TEXT;
  v_max_class_ts TIMESTAMPTZ;
  v_now       TIMESTAMPTZ := now();
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  v_kid   := p_envelope->>'kid';
  v_nonce := p_envelope->>'nonce';
  v_iat   := (p_envelope->>'iat')::BIGINT;

  IF v_kid IS NULL OR v_nonce IS NULL THEN
    RAISE EXCEPTION 'malformed_envelope' USING ERRCODE = '22023';
  END IF;

  IF v_iat IS NULL OR ABS(EXTRACT(EPOCH FROM v_now)::BIGINT - v_iat) > 300 THEN
    RAISE EXCEPTION 'iat_window' USING ERRCODE = '22023';
  END IF;

  v_device_id := v_kid::UUID;
  IF NOT EXISTS (
    SELECT 1 FROM devices.registered_devices
    WHERE id = v_device_id
      AND lenser_id = v_lenser_id
      AND trust_level IN ('approved','trusted')
  ) THEN
    RAISE EXCEPTION 'kid_mismatch' USING ERRCODE = '42501';
  END IF;

  BEGIN
    INSERT INTO devices.nonce_cache (nonce, device_id, expires_at)
    VALUES (v_nonce, v_device_id, v_now + interval '10 minutes');
  EXCEPTION WHEN unique_violation THEN
    RAISE EXCEPTION 'nonce_replay' USING ERRCODE = '23505';
  END;

  p_limit := LEAST(COALESCE(p_limit, 100), 200);

  -- Return rows from the requested classes that are newer than the device's
  -- per-class watermark, then advance each watermark.
  CREATE TEMP TABLE _sync_pull_result ON COMMIT DROP AS
  SELECT
    o.id,
    o.object_class,
    o.object_id,
    o.op,
    o.payload,
    o.vclock,
    o.created_at
  FROM devices.sync_outbox o
  LEFT JOIN devices.sync_watermarks w
    ON w.device_id    = v_device_id
   AND w.object_class = o.object_class
  WHERE o.lenser_id = v_lenser_id
    AND o.device_id <> v_device_id
    AND o.object_class = ANY(p_object_classes)
    AND o.created_at > COALESCE(w.watermark, '-infinity'::timestamptz)
  ORDER BY o.created_at ASC
  LIMIT p_limit;

  -- Advance watermarks atomically per class (max created_at returned).
  FOR v_class IN SELECT DISTINCT r.object_class FROM _sync_pull_result r
  LOOP
    SELECT MAX(created_at) INTO v_max_class_ts
    FROM _sync_pull_result
    WHERE object_class = v_class;

    INSERT INTO devices.sync_watermarks (lenser_id, device_id, object_class, watermark, updated_at)
    VALUES (v_lenser_id, v_device_id, v_class, v_max_class_ts, v_now)
    ON CONFLICT (device_id, object_class) DO UPDATE
      SET watermark = GREATEST(devices.sync_watermarks.watermark, EXCLUDED.watermark),
          updated_at = v_now;
  END LOOP;

  RETURN QUERY SELECT * FROM _sync_pull_result ORDER BY created_at ASC;
END;
$$;


ALTER FUNCTION "devices"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_sync_push"("p_envelope" "jsonb") RETURNS TABLE("applied_count" integer, "rejected_count" integer, "rejections" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_device_id UUID;
  v_kid       TEXT;
  v_nonce     TEXT;
  v_iat       BIGINT;
  v_entries   JSONB;
  v_entry     JSONB;
  v_class     TEXT;
  v_object_id TEXT;
  v_op        TEXT;
  v_payload   JSONB;
  v_vclock    JSONB;
  v_applied   INT := 0;
  v_rejected  INT := 0;
  v_reasons   JSONB := '[]';
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  v_kid   := p_envelope->>'kid';
  v_nonce := p_envelope->>'nonce';
  v_iat   := (p_envelope->>'iat')::BIGINT;
  v_entries := p_envelope->'body'->'entries';

  IF v_kid IS NULL OR v_nonce IS NULL OR v_entries IS NULL THEN
    RAISE EXCEPTION 'malformed_envelope' USING ERRCODE = '22023';
  END IF;

  -- iat ±300s window
  IF v_iat IS NULL OR ABS(EXTRACT(EPOCH FROM now())::BIGINT - v_iat) > 300 THEN
    RAISE EXCEPTION 'iat_window' USING ERRCODE = '22023';
  END IF;

  -- Resolve device + ownership.
  v_device_id := v_kid::UUID;
  IF NOT EXISTS (
    SELECT 1 FROM devices.registered_devices
    WHERE id = v_device_id
      AND lenser_id = v_lenser_id
      AND trust_level IN ('approved','trusted')
  ) THEN
    RAISE EXCEPTION 'kid_mismatch' USING ERRCODE = '42501';
  END IF;

  -- Replay protection via nonce_cache.
  BEGIN
    INSERT INTO devices.nonce_cache (nonce, device_id, expires_at)
    VALUES (v_nonce, v_device_id, now() + interval '10 minutes');
  EXCEPTION WHEN unique_violation THEN
    RAISE EXCEPTION 'nonce_replay' USING ERRCODE = '23505';
  END;

  FOR v_entry IN SELECT * FROM jsonb_array_elements(v_entries)
  LOOP
    v_class := v_entry->>'object_class';
    v_object_id := v_entry->>'object_id';
    v_op := v_entry->>'op';
    v_payload := COALESCE(v_entry->'payload', '{}'::jsonb);
    v_vclock := COALESCE(v_entry->'vclock', '{}'::jsonb);

    IF v_class IS NULL OR v_object_id IS NULL OR v_op IS NULL THEN
      v_rejected := v_rejected + 1;
      v_reasons  := v_reasons || jsonb_build_object('object_id', v_object_id, 'reason', 'malformed_entry');
      CONTINUE;
    END IF;

    -- Reject cloud-authoritative classes (write attempts on edges).
    IF v_class IN ('xp_total','trust_evaluation','battle_result','policy',
                   'budget','kill_switch','dark_launch','ai_catalog') THEN
      v_rejected := v_rejected + 1;
      v_reasons  := v_reasons || jsonb_build_object('object_id', v_object_id, 'reason', 'cloud_authoritative');
      CONTINUE;
    END IF;

    -- Reject local-only classes (must never be pushed).
    IF v_class IN ('byok_key','local_battle','scratchpad_draft',
                   'keychain_entry','private_key') THEN
      v_rejected := v_rejected + 1;
      v_reasons  := v_reasons || jsonb_build_object('object_id', v_object_id, 'reason', 'local_only_class');
      CONTINUE;
    END IF;

    INSERT INTO devices.sync_outbox (
      lenser_id, device_id, object_class, object_id, op, payload, vclock
    ) VALUES (
      v_lenser_id, v_device_id, v_class, v_object_id, v_op, v_payload, v_vclock
    );
    v_applied := v_applied + 1;
  END LOOP;

  RETURN QUERY SELECT v_applied, v_rejected, v_reasons;
END;
$$;


ALTER FUNCTION "devices"."fn_sync_push"("p_envelope" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_sync_resolve_conflict"("p_outbox_id" "uuid", "p_winner" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  UPDATE devices.sync_outbox
     SET payload   = p_winner,
         applied_at = now()
   WHERE id = p_outbox_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'conflict_not_found' USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "devices"."fn_sync_resolve_conflict"("p_outbox_id" "uuid", "p_winner" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_sync_status"("p_device_id" "uuid") RETURNS TABLE("object_class" "text", "watermark" timestamp with time zone, "outbox_depth" bigint, "last_error" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    w.object_class,
    w.watermark,
    (SELECT COUNT(*)
       FROM devices.sync_outbox o
      WHERE o.device_id = p_device_id
        AND o.object_class = w.object_class
        AND o.applied_at IS NULL)::BIGINT AS outbox_depth,
    NULL::TEXT AS last_error
  FROM devices.sync_watermarks w
  WHERE w.lenser_id = v_lenser_id
    AND w.device_id = p_device_id
  ORDER BY w.object_class;
END;
$$;


ALTER FUNCTION "devices"."fn_sync_status"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_xp_on_device_registered"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'xp', 'execution', 'public', 'extensions'
    AS $$
BEGIN
  PERFORM execution.fn_xp_apply_safe(
    NEW.lenser_id,
    'DEVICE_REGISTERED',
    'system'::xp.source_enum,
    'device',
    NEW.id,
    '00000000-0000-0000-0000-000000000003'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "devices"."fn_xp_on_device_registered"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "devices"."fn_xp_on_device_trust_elevated"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'devices', 'xp', 'execution', 'public', 'extensions'
    AS $$
BEGIN
  IF NEW.trust_level IN ('approved', 'trusted')
     AND COALESCE(OLD.trust_level, 'pending') NOT IN ('approved', 'trusted')
  THEN
    PERFORM execution.fn_xp_apply_safe(
      NEW.lenser_id,
      'DEVICE_VERIFIED',
      'system'::xp.source_enum,
      'device',
      NEW.id,
      '00000000-0000-0000-0000-000000000003'::uuid
    );
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "devices"."fn_xp_on_device_trust_elevated"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_async_run_idempotent_complete"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint DEFAULT NULL::bigint, "p_width" integer DEFAULT NULL::integer, "p_height" integer DEFAULT NULL::integer, "p_duration_s" numeric DEFAULT NULL::numeric) RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
DECLARE
  v_status TEXT;
BEGIN
  SELECT status INTO v_status
  FROM execution.runs
  WHERE id = p_run_id
  FOR UPDATE;

  IF v_status IS NULL THEN
    RETURN FALSE;  -- run does not exist
  END IF;

  -- Already terminal — no-op success so the worker can ack.
  IF v_status IN ('succeeded', 'failed', 'canceled', 'timed_out') THEN
    RETURN FALSE;
  END IF;

  PERFORM execution.fn_complete_async_run(
    p_run_id, p_media_url, p_mime_type, p_bytes, p_width, p_height, p_duration_s
  );
  RETURN TRUE;
END;
$$;


ALTER FUNCTION "execution"."fn_async_run_idempotent_complete"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_async_run_idempotent_complete"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) IS 'Phase AM: idempotent wrapper around fn_complete_async_run. Returns TRUE when the run was transitioned, FALSE when the run is already terminal (no-op) or does not exist. Lets poll workers retry without producing duplicate media.objects rows.';



CREATE OR REPLACE FUNCTION "execution"."fn_b64url_decode"("p_in" "text") RETURNS "bytea"
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'pg_catalog', 'public'
    AS $$
DECLARE
  v_padded TEXT;
  v_pad_len INT;
BEGIN
  IF p_in IS NULL OR length(p_in) = 0 THEN
    RETURN NULL;
  END IF;
  v_padded := translate(p_in, '-_', '+/');
  v_pad_len := (4 - (length(v_padded) % 4)) % 4;
  IF v_pad_len > 0 THEN
    v_padded := v_padded || repeat('=', v_pad_len);
  END IF;
  RETURN decode(v_padded, 'base64');
END;
$$;


ALTER FUNCTION "execution"."fn_b64url_decode"("p_in" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_b64url_decode"("p_in" "text") IS 'Decode RFC 4648 base64url (no padding) to bytea. Used by signature verification helpers.';



CREATE OR REPLACE FUNCTION "execution"."fn_chain_on_attestation_verified"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'audit', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT rd.lenser_id INTO v_lenser_id
  FROM devices.registered_devices rd
  WHERE rd.id = NEW.device_id;

  IF v_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  PERFORM audit.fn_append_gateway_event_safe(
    CASE WHEN NEW.verified
         THEN 'GATEWAY_ATTESTATION_VERIFIED'
         ELSE 'GATEWAY_ATTESTATION_REJECTED'
    END,
    v_lenser_id,
    NEW.device_id,
    jsonb_build_object(
      'attestation_id', NEW.attestation_id,
      'envelope_kid',   NEW.envelope_kid,
      'envelope_iat',   NEW.envelope_iat,
      'invalid_reason', NEW.invalid_reason
    )
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "execution"."fn_chain_on_attestation_verified"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_claim_queued_run"() RETURNS TABLE("run_id" "uuid", "request_id" "uuid", "requester_lenser_id" "uuid", "workspace_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "model_id" "uuid", "model_key" "text", "provider_key" "text", "funding_source" "text", "byok_key_ref_id" "uuid", "input_snapshot" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'ai', 'public'
    AS $$
DECLARE
  v_run_id uuid;
BEGIN
  IF auth.role() IS DISTINCT FROM 'service_role' THEN
    RAISE EXCEPTION 'Forbidden: only service_role can claim queued runs';
  END IF;

  WITH next_run AS (
    SELECT r.id
    FROM execution.runs r
    JOIN execution.requests req
      ON req.id = r.request_id
    WHERE r.status = 'queued'
      AND r.is_active = true
      AND req.is_active = true
    ORDER BY r.created_at ASC
    FOR UPDATE SKIP LOCKED
    LIMIT 1
  )
  UPDATE execution.runs r
     SET status = 'running',
         started_at = COALESCE(r.started_at, now())
    FROM next_run nr
   WHERE r.id = nr.id
  RETURNING r.id INTO v_run_id;

  IF v_run_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT
    r.id AS run_id,
    req.id AS request_id,
    req.requester_lenser_id,
    req.workspace_id,
    req.lens_id,
    req.version_id,
    r.model_id,
    m.key AS model_key,
    p.key AS provider_key,
    req.funding_source,
    req.byok_key_ref_id,
    req.input_snapshot
  FROM execution.runs r
  JOIN execution.requests req
    ON req.id = r.request_id
  LEFT JOIN ai.models m
    ON m.id = r.model_id
  LEFT JOIN ai.providers p
    ON p.id = m.provider_id
  WHERE r.id = v_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_claim_queued_run"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_complete_async_run"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint DEFAULT NULL::bigint, "p_width" integer DEFAULT NULL::integer, "p_height" integer DEFAULT NULL::integer, "p_duration_s" numeric DEFAULT NULL::numeric) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'media', 'public'
    AS $$
DECLARE
  v_request_id      UUID;
  v_model_id        UUID;
  v_output_modality TEXT;
  v_media_object_id UUID;
  v_credit_cost     NUMERIC := 0;
  v_rate            NUMERIC;
  v_rate_unit       TEXT;
  v_media_type      TEXT;
BEGIN
  -- Lock the run row
  SELECT request_id INTO v_request_id
  FROM execution.runs
  WHERE id = p_run_id AND is_async = TRUE AND status = 'running'
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Run % not found or not in running/async state', p_run_id;
  END IF;

  -- Pull model_id and output_modality from the request
  SELECT model_id, output_modality
  INTO v_model_id, v_output_modality
  FROM execution.requests
  WHERE id = v_request_id;

  -- Resolve credit cost from modality_pricing
  SELECT credit_rate, rate_unit
  INTO v_rate, v_rate_unit
  FROM ai.modality_pricing
  WHERE model_id = v_model_id
    AND output_modality = COALESCE(v_output_modality, 'image')
    AND is_active = TRUE
  LIMIT 1;

  IF FOUND THEN
    v_credit_cost := CASE v_rate_unit
      WHEN 'per_image'    THEN v_rate
      WHEN 'per_second'   THEN v_rate * COALESCE(p_duration_s, 0)
      WHEN 'per_1k_chars' THEN 0
      WHEN 'per_request'  THEN v_rate
      ELSE 0
    END;
  END IF;

  -- Determine media_type for media.objects
  v_media_type := CASE
    WHEN p_mime_type LIKE 'image/%' THEN 'image'
    WHEN p_mime_type LIKE 'video/%' THEN 'video'
    WHEN p_mime_type LIKE 'audio/%' THEN 'audio'
    ELSE 'binary'
  END;

  -- Create media.objects row — AN: populate first-class dimension/duration columns
  INSERT INTO media.objects (
    bucket,
    object_key,
    external_url,
    mime_type,
    media_type,
    byte_size,
    visibility,
    lifecycle_state,
    metadata,
    request_id,
    duration_seconds,
    video_width,
    video_height
  )
  VALUES (
    'generated-media',
    'async/' || p_run_id::TEXT || '.' || split_part(p_mime_type, '/', 2),
    p_media_url,
    p_mime_type,
    v_media_type,
    p_bytes,
    'private',
    'active',
    jsonb_build_object(
      'width',      p_width,
      'height',     p_height,
      'duration_s', p_duration_s
    ),
    v_request_id,
    p_duration_s,
    p_width,
    p_height
  )
  RETURNING id INTO v_media_object_id;

  -- Write primary artifact
  INSERT INTO execution.artifacts (
    run_id,
    artifact_kind,
    content_text,
    visibility,
    is_primary_output,
    media_object_id,
    output_type
  )
  VALUES (
    p_run_id,
    COALESCE(v_output_modality, 'image')::TEXT,
    p_media_url,
    'private',
    TRUE,
    v_media_object_id,
    COALESCE(v_output_modality, 'image')
  );

  -- Mark run succeeded
  UPDATE execution.runs
  SET
    status         = 'succeeded',
    completed_at   = now(),
    latency_ms     = EXTRACT(EPOCH FROM (now() - started_at))::BIGINT * 1000,
    credit_cost    = v_credit_cost,
    billing_status = CASE WHEN v_credit_cost > 0 THEN 'pending' ELSE 'free' END
  WHERE id = p_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_complete_async_run"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_complete_async_run"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) IS 'AN: Updated to populate media.objects.duration_seconds, video_width, video_height. Called by poll-async-executions Edge Function when a video/audio provider signals done.';



CREATE OR REPLACE FUNCTION "execution"."fn_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer DEFAULT NULL::integer, "p_token_output" integer DEFAULT NULL::integer, "p_credit_cost" bigint DEFAULT NULL::bigint, "p_billing_status" "text" DEFAULT 'free'::"text", "p_response_text" "text" DEFAULT NULL::"text", "p_response_meta" "jsonb" DEFAULT '{}'::"jsonb", "p_error_code" "text" DEFAULT NULL::"text", "p_error_message" "text" DEFAULT NULL::"text", "p_latency_ms" integer DEFAULT NULL::integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
BEGIN
    UPDATE "execution"."runs"
    SET
        "status"         = p_status,
        "token_input"    = COALESCE(p_token_input,    "token_input"),
        "token_output"   = COALESCE(p_token_output,   "token_output"),
        "credit_cost"    = COALESCE(p_credit_cost,    "credit_cost"),
        "billing_status" = p_billing_status,
        "response_text"  = p_response_text,
        "response_meta"  = COALESCE(p_response_meta,  "response_meta"),
        "error_code"     = p_error_code,
        "error_message"  = p_error_message,
        "latency_ms"     = COALESCE(p_latency_ms,     "latency_ms"),
        "completed_at"   = now()
    WHERE "id" = p_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) IS 'Updates execution.runs with final status, token counts, credit cost, and response. Called fire-and-forget after provider execution completes or fails. SECURITY DEFINER; callable only by service_role.';



CREATE OR REPLACE FUNCTION "execution"."fn_compute_submission_trust"("p_submission_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'battles', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_run_id            UUID;
  v_attestation       execution.attestations%ROWTYPE;
  v_verification      execution.attestation_verifications%ROWTYPE;
  v_trust_level       TEXT := 'unverified';
  v_factors           JSONB := '{}';
  v_account_ok        BOOLEAN := false;
  v_agent_ok          BOOLEAN := false;
  v_signature_verified BOOLEAN := false;
BEGIN
  -- Account / run ownership
  SELECT er.id INTO v_run_id
  FROM battles.submissions s
  JOIN execution.runs er ON er.id = s.execution_run_id
  WHERE s.id = p_submission_id
  LIMIT 1;

  v_account_ok := (v_run_id IS NOT NULL);
  v_factors := v_factors || jsonb_build_object('account_authenticated', v_account_ok);

  IF NOT v_account_ok THEN
    INSERT INTO execution.trust_evaluations (submission_id, trust_level, factors, evaluated_at)
    VALUES (p_submission_id, 'unverified', v_factors, now())
    ON CONFLICT (submission_id) DO UPDATE
      SET trust_level = EXCLUDED.trust_level,
          factors = EXCLUDED.factors,
          evaluated_at = EXCLUDED.evaluated_at;
    RETURN 'unverified';
  END IF;

  v_trust_level := 'account_verified';

  SELECT EXISTS (
    SELECT 1 FROM battles.contenders bc
    WHERE bc.battle_id = (SELECT battle_id FROM battles.submissions WHERE id = p_submission_id)
      AND bc.contender_type IN ('ai_agent','ai_runner')
  ) INTO v_agent_ok;
  v_factors := v_factors || jsonb_build_object('agent_owner_verified', v_agent_ok);
  IF v_agent_ok THEN v_trust_level := 'agent_verified'; END IF;

  -- Latest attestation for this run
  SELECT * INTO v_attestation
  FROM execution.attestations
  WHERE run_id = v_run_id
  ORDER BY created_at DESC
  LIMIT 1;

  IF v_attestation.id IS NOT NULL THEN
    SELECT * INTO v_verification
    FROM execution.attestation_verifications
    WHERE attestation_id = v_attestation.id;

    v_signature_verified := COALESCE(v_verification.verified, false);

    v_factors := v_factors || jsonb_build_object(
      'device_trusted',         v_attestation.device_trusted,
      'runner_connected',       v_attestation.device_id IS NOT NULL,
      'execution_signed',       v_attestation.signed,
      'signature_verified',     v_signature_verified,
      'signature_invalid_reason', v_verification.invalid_reason,
      'policy_passed',          v_attestation.policy_passed,
      'workflow_hash_recorded', v_attestation.workflow_hash IS NOT NULL,
      'lens_hash_recorded',     v_attestation.lens_hash IS NOT NULL,
      'envelope_recorded',      v_attestation.envelope_kid IS NOT NULL
    );

    IF v_attestation.device_trusted THEN
      v_trust_level := 'device_verified';
    END IF;
    IF v_attestation.device_id IS NOT NULL AND v_attestation.device_trusted THEN
      v_trust_level := 'runner_verified';
    END IF;

    -- Phase F: execution_verified now requires a positive signature verification.
    -- The legacy `gateway_verified` flag is no longer accepted.
    IF v_attestation.signed AND v_signature_verified THEN
      v_trust_level := 'execution_verified';
    END IF;

    IF v_attestation.signed
       AND v_signature_verified
       AND v_attestation.device_trusted
       AND v_attestation.policy_passed
       AND v_attestation.workflow_hash IS NOT NULL
       AND v_attestation.lens_hash IS NOT NULL
       AND v_attestation.envelope_kid IS NOT NULL
    THEN
      v_trust_level := 'fully_trusted';
    END IF;
  END IF;

  INSERT INTO execution.trust_evaluations (
    submission_id, attestation_id, trust_level, factors, evaluated_at
  ) VALUES (
    p_submission_id,
    v_attestation.id,
    v_trust_level,
    v_factors,
    now()
  )
  ON CONFLICT (submission_id) DO UPDATE
    SET attestation_id = EXCLUDED.attestation_id,
        trust_level    = EXCLUDED.trust_level,
        factors        = EXCLUDED.factors,
        evaluated_at   = EXCLUDED.evaluated_at;

  RETURN v_trust_level;
END;
$$;


ALTER FUNCTION "execution"."fn_compute_submission_trust"("p_submission_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("request_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "version_number" integer, "model_id" "uuid", "model_key" "text", "provider_key" "text", "funding_source" "text", "run_id" "uuid", "run_status" "text", "latency_ms" integer, "token_input" integer, "token_output" integer, "credit_cost" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lenses', 'ai', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid := "lensers"."get_auth_lenser_id"();
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  RETURN QUERY
  SELECT
    req."id"                    AS request_id,
    req."lens_id"               AS lens_id,
    req."version_id"            AS version_id,
    lv."version_number"         AS version_number,
    req."model_id"              AS model_id,
    m."key"                     AS model_key,
    p."key"                     AS provider_key,
    req."funding_source"        AS funding_source,
    r."id"                      AS run_id,
    r."status"                  AS run_status,
    r."latency_ms"              AS latency_ms,
    r."token_input"             AS token_input,
    r."token_output"            AS token_output,
    r."credit_cost"             AS credit_cost,
    req."created_at"            AS created_at
  FROM  "execution"."requests"  req
  LEFT JOIN "execution"."runs"  r   ON r."request_id"  = req."id"
                                   AND r."is_active"   = true
  LEFT JOIN "lenses"."versions" lv  ON lv."id"         = req."version_id"
  LEFT JOIN "ai"."models"       m   ON m."id"          = req."model_id"
  LEFT JOIN "ai"."providers"    p   ON p."id"          = m."provider_id"
  WHERE req."lens_id"              = p_lens_id
    AND req."requester_lenser_id"  = v_caller_id
    AND req."is_active"            = true
  ORDER BY req."created_at" DESC
  LIMIT  p_limit
  OFFSET p_offset;
END;
$$;


ALTER FUNCTION "execution"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'GRASP Controller: returns caller-scoped execution history for a lens, enriched with model key, provider key, and version number. Used by LabExecutionTimeline to display model/provider/version badges. SECURITY DEFINER: filters to requester_lenser_id only. Replaces the deprecated execution.vw_ray_runs (compatibility shim) for app-layer reads.';



CREATE OR REPLACE FUNCTION "execution"."fn_get_run_details"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "request_id" "uuid", "status" "text", "model_id" "uuid", "model_key" "text", "provider_key" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "latency_ms" integer, "token_input" integer, "token_output" integer, "credit_cost" bigint, "billing_status" "text", "error_code" "text", "error_message" "text", "artifacts" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lenses', 'lensers', 'ai', 'public'
    AS $$
DECLARE
  v_caller_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  RETURN QUERY
  SELECT
    r.id,
    r.request_id,
    r.status,
    r.model_id,
    m.key AS model_key,
    p.key AS provider_key,
    r.started_at,
    r.completed_at,
    r.latency_ms,
    r.token_input,
    r.token_output,
    r.credit_cost,
    r.billing_status,
    r.error_code,
    r.error_message,
    COALESCE(
      jsonb_agg(
        jsonb_build_object(
          'id', a.id,
          'artifactKind', a.artifact_kind,
          'contentText', a.content_text,
          'contentJson', a.content_json,
          'visibility', a.visibility,
          'isPrimaryOutput', a.is_primary_output,
          'createdAt', a.created_at
        )
      ) FILTER (WHERE a.id IS NOT NULL),
      '[]'::jsonb
    ) AS artifacts
  FROM execution.runs r
  JOIN execution.requests req
    ON req.id = r.request_id
  LEFT JOIN ai.models m
    ON m.id = r.model_id
  LEFT JOIN ai.providers p
    ON p.id = m.provider_id
  LEFT JOIN execution.artifacts a
    ON a.run_id = r.id
  WHERE r.id = p_run_id
    AND req.requester_lenser_id = v_caller_id
    AND req.is_active = true
    AND r.is_active = true
  GROUP BY
    r.id, r.request_id, r.status, r.model_id, m.key, p.key, r.started_at,
    r.completed_at, r.latency_ms, r.token_input, r.token_output, r.credit_cost,
    r.billing_status, r.error_code, r.error_message;
END;
$$;


ALTER FUNCTION "execution"."fn_get_run_details"("p_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_get_submission_trust"("p_submission_id" "uuid") RETURNS TABLE("submission_id" "uuid", "trust_level" "text", "factors" "jsonb", "attestation_id" "uuid", "evaluated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'battles', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_caller_lenser_id UUID;
  v_owner_lenser_id  UUID;
BEGIN
  SELECT lp.id INTO v_caller_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
  LIMIT 1;

  SELECT req.requester_lenser_id INTO v_owner_lenser_id
  FROM battles.submissions s
  JOIN execution.runs r       ON r.id   = s.execution_run_id
  JOIN execution.requests req ON req.id = r.request_id
  WHERE s.id = p_submission_id
  LIMIT 1;

  RETURN QUERY
  SELECT
    te.submission_id,
    te.trust_level,
    CASE
      WHEN v_caller_lenser_id IS NOT NULL
       AND v_caller_lenser_id = v_owner_lenser_id THEN te.factors
      ELSE jsonb_build_object('redacted', true)
    END AS factors,
    CASE
      WHEN v_caller_lenser_id IS NOT NULL
       AND v_caller_lenser_id = v_owner_lenser_id THEN te.attestation_id
      ELSE NULL::UUID
    END AS attestation_id,
    te.evaluated_at
  FROM execution.trust_evaluations te
  WHERE te.submission_id = p_submission_id;
END;
$$;


ALTER FUNCTION "execution"."fn_get_submission_trust"("p_submission_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_get_submission_trust"("p_submission_id" "uuid") IS 'Phase A (RFC-0003): owner-scoped read of submission trust. Anonymous and non-owner callers receive a redacted factors object. Direct table reads are no longer permitted by RLS.';



CREATE OR REPLACE FUNCTION "execution"."fn_media_finalize_sync_upload"("p_run_id" "uuid", "p_object_key" "text", "p_mime_type" "text", "p_bytes" bigint DEFAULT NULL::bigint, "p_width" integer DEFAULT NULL::integer, "p_height" integer DEFAULT NULL::integer, "p_duration_s" numeric DEFAULT NULL::numeric) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'media', 'ai', 'public'
    AS $$
DECLARE
  v_request_id      UUID;
  v_model_id        UUID;
  v_output_modality TEXT;
  v_owner_lenser_id UUID;
  v_workspace_id    UUID;
  v_media_object_id UUID;
  v_credit_cost     NUMERIC := 0;
  v_rate            NUMERIC;
  v_rate_unit       TEXT;
  v_media_type      TEXT;
BEGIN
  -- Validate MIME type against the bucket's allowed list. We re-check at the
  -- RPC layer because storage.objects-level enforcement happens at upload
  -- time and a buggy worker might call this RPC with a mismatched type.
  IF p_mime_type IS NULL OR NOT (
    p_mime_type LIKE 'image/%' OR
    p_mime_type LIKE 'video/%' OR
    p_mime_type LIKE 'audio/%'
  ) THEN
    RAISE EXCEPTION 'Unsupported MIME type for sync media finalize: %', p_mime_type
      USING ERRCODE = 'check_violation';
  END IF;

  IF p_object_key IS NULL OR length(p_object_key) = 0 THEN
    RAISE EXCEPTION 'object_key is required'
      USING ERRCODE = 'check_violation';
  END IF;

  -- Lock the running row. Sync runs are typically `is_async = FALSE` but we
  -- accept both — the discriminator is `status = 'running'`.
  SELECT request_id
  INTO v_request_id
  FROM execution.runs
  WHERE id = p_run_id AND status = 'running'
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Run % not found or not in running state', p_run_id;
  END IF;

  -- requester_lenser_id + workspace_id live on execution.requests (not runs).
  SELECT model_id, output_modality, requester_lenser_id, workspace_id
  INTO v_model_id, v_output_modality, v_owner_lenser_id, v_workspace_id
  FROM execution.requests
  WHERE id = v_request_id;

  -- Credit cost lookup mirrors fn_complete_async_run exactly.
  SELECT credit_rate, rate_unit
  INTO v_rate, v_rate_unit
  FROM ai.modality_pricing
  WHERE model_id = v_model_id
    AND output_modality = COALESCE(v_output_modality, 'image')
    AND is_active = TRUE
  LIMIT 1;

  IF FOUND THEN
    v_credit_cost := CASE v_rate_unit
      WHEN 'per_image'    THEN v_rate
      WHEN 'per_second'   THEN v_rate * COALESCE(p_duration_s, 0)
      WHEN 'per_1k_chars' THEN 0
      WHEN 'per_request'  THEN v_rate
      ELSE 0
    END;
  END IF;

  v_media_type := CASE
    WHEN p_mime_type LIKE 'image/%' THEN 'image'
    WHEN p_mime_type LIKE 'video/%' THEN 'video'
    WHEN p_mime_type LIKE 'audio/%' THEN 'audio'
    ELSE 'binary'
  END;

  INSERT INTO media.objects (
    workspace_id,
    owner_lenser_id,
    bucket,
    object_key,
    mime_type,
    media_type,
    byte_size,
    visibility,
    lifecycle_state,
    metadata,
    request_id
  )
  VALUES (
    v_workspace_id,
    v_owner_lenser_id,
    'generated-media',
    p_object_key,
    p_mime_type,
    v_media_type,
    p_bytes,
    'private',
    'active',
    jsonb_build_object(
      'width', p_width,
      'height', p_height,
      'duration_s', p_duration_s,
      'finalize_path', 'sync'
    ),
    v_request_id
  )
  RETURNING id INTO v_media_object_id;

  INSERT INTO execution.artifacts (
    run_id,
    artifact_kind,
    content_text,
    visibility,
    is_primary_output,
    media_object_id,
    output_type
  )
  VALUES (
    p_run_id,
    COALESCE(v_output_modality, 'image')::TEXT,
    NULL,                       -- sync uploads have no external_url
    'private',
    TRUE,
    v_media_object_id,
    COALESCE(v_output_modality, 'image')
  );

  UPDATE execution.runs
  SET status         = 'succeeded',
      completed_at   = now(),
      latency_ms     = EXTRACT(EPOCH FROM (now() - started_at))::BIGINT * 1000,
      credit_cost    = v_credit_cost,
      billing_status = CASE WHEN v_credit_cost > 0 THEN 'pending' ELSE 'free' END
  WHERE id = p_run_id;

  RETURN v_media_object_id;
END;
$$;


ALTER FUNCTION "execution"."fn_media_finalize_sync_upload"("p_run_id" "uuid", "p_object_key" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_media_finalize_sync_upload"("p_run_id" "uuid", "p_object_key" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) IS 'Phase AK: sync-provider counterpart to fn_complete_async_run. Called by the platform-api worker after uploading a synchronously-generated asset (TTS, sync image) to the generated-media bucket. Inserts media.objects + execution.artifacts, computes credit cost via ai.modality_pricing, and marks the run succeeded. service_role only.';



CREATE OR REPLACE FUNCTION "execution"."fn_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text" DEFAULT NULL::"text", "p_content_json" "jsonb" DEFAULT NULL::"jsonb", "p_media_ids" "uuid"[] DEFAULT '{}'::"uuid"[]) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
DECLARE
    v_artifact_id "uuid";
    v_media_id    "uuid";
BEGIN
    INSERT INTO "execution"."artifacts" (
        "run_id",
        "artifact_kind",
        "content_text",
        "content_json",
        "visibility",
        "is_primary_output",
        "output_type"
    ) VALUES (
        p_run_id,
        p_kind,
        p_content_text,
        p_content_json,
        'private',
        true,
        p_kind
    )
    RETURNING "id" INTO v_artifact_id;

    -- Link each media object
    FOREACH v_media_id IN ARRAY p_media_ids LOOP
        INSERT INTO "execution"."artifact_medias" ("artifact_id", "media_id")
        VALUES (v_artifact_id, v_media_id);
    END LOOP;

    RETURN v_artifact_id;
END;
$$;


ALTER FUNCTION "execution"."fn_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) IS 'Persist an execution artifact and link N media objects (images, audio, video). p_media_ids defaults to empty array for text-only artifacts. SECURITY DEFINER; callable only by service_role.';



CREATE OR REPLACE FUNCTION "execution"."fn_persist_local_execution"("p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_provider" "text" DEFAULT 'ollama'::"text", "p_model" "text" DEFAULT ''::"text", "p_content_text" "text" DEFAULT ''::"text", "p_token_input" integer DEFAULT 0, "p_token_output" integer DEFAULT 0) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lensers', 'public'
    AS $$
DECLARE
    v_caller_id   uuid := "lensers"."get_auth_lenser_id"();
    v_request_id  uuid;
    v_run_id      uuid;
    v_now         timestamptz := now();
BEGIN
    IF v_caller_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required'
            USING ERRCODE = 'insufficient_privilege';
    END IF;

    -- 1. Create the execution request
    INSERT INTO "execution"."requests" (
        "requester_lenser_id",
        "origin_type",
        "lens_id",
        "version_id",
        "input_snapshot",
        "runtime_origin",
        "funding_source",
        "created_at"
    ) VALUES (
        v_caller_id,
        'lens_preview',
        p_lens_id,
        p_version_id,
        '{}'::jsonb,
        'local',
        'user_byok_local',
        v_now
    )
    RETURNING "id" INTO v_request_id;

    -- 2. Create the run record (already completed)
    INSERT INTO "execution"."runs" (
        "request_id",
        "status",
        "started_at",
        "completed_at",
        "token_input",
        "token_output",
        "credit_cost",
        "billing_status",
        "response_text",
        "response_meta",
        "created_at"
    ) VALUES (
        v_request_id,
        'succeeded',
        v_now,
        v_now,
        p_token_input,
        p_token_output,
        0,
        'free',
        p_content_text,
        jsonb_build_object('provider', p_provider, 'model', p_model, 'source', 'local_byok'),
        v_now
    )
    RETURNING "id" INTO v_run_id;

    -- 3. Create the primary text artifact
    INSERT INTO "execution"."artifacts" (
        "run_id",
        "artifact_kind",
        "content_text",
        "visibility",
        "is_primary_output",
        "output_type",
        "created_at"
    ) VALUES (
        v_run_id,
        'text',
        p_content_text,
        'private',
        true,
        'text',
        v_now
    );

    RETURN v_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_persist_local_execution"("p_lens_id" "uuid", "p_version_id" "uuid", "p_provider" "text", "p_model" "text", "p_content_text" "text", "p_token_input" integer, "p_token_output" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_persist_local_execution"("p_lens_id" "uuid", "p_version_id" "uuid", "p_provider" "text", "p_model" "text", "p_content_text" "text", "p_token_input" integer, "p_token_output" integer) IS 'Persists a completed local BYOK execution (browser-side streaming) into execution.requests + runs + artifacts. Called from the client after streamLocalProvider finishes so that local runs appear in execution history and survive page refresh. SECURITY DEFINER: resolves requester from auth.uid(). Credits always 0 for local runs.';



CREATE OR REPLACE FUNCTION "execution"."fn_poll_async_run"("p_stale_after_seconds" integer DEFAULT 30, "p_limit" integer DEFAULT 10) RETURNS TABLE("run_id" "uuid", "provider_task_id" "text", "model_key" "text", "provider_key" "text", "output_modality" "text", "started_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'ai', 'public'
    AS $$
DECLARE
  v_ids UUID[];
BEGIN
  -- 1. Claim a batch of due runs with SKIP LOCKED. Order by NULLS FIRST so
  -- runs that have never been polled get priority over recently-polled ones.
  WITH claimed AS (
    SELECT r.id
    FROM execution.runs r
    WHERE r.is_async = TRUE
      AND r.status   = 'running'
      AND r.provider_task_id IS NOT NULL
      AND (
        r.last_polled_at IS NULL
        OR r.last_polled_at < now() - (p_stale_after_seconds || ' seconds')::INTERVAL
      )
    ORDER BY r.last_polled_at NULLS FIRST, r.started_at
    FOR UPDATE SKIP LOCKED
    LIMIT GREATEST(1, LEAST(p_limit, 50))
  )
  SELECT array_agg(id) INTO v_ids FROM claimed;

  IF v_ids IS NULL THEN
    RETURN;  -- nothing to poll; quiet exit
  END IF;

  -- 2. Bump last_polled_at on the claimed rows so the next tick avoids them
  -- until p_stale_after_seconds has elapsed.
  UPDATE execution.runs
  SET last_polled_at = now()
  WHERE id = ANY(v_ids);

  -- 3. Return the join shape the worker expects.
  RETURN QUERY
    SELECT
      r.id              AS run_id,
      r.provider_task_id,
      m.key             AS model_key,
      p.key             AS provider_key,
      req.output_modality,
      r.started_at
    FROM execution.runs r
    JOIN execution.requests req ON req.id = r.request_id
    LEFT JOIN ai.models   m ON m.id = req.model_id
    LEFT JOIN ai.providers p ON p.id = m.provider_id
    WHERE r.id = ANY(v_ids)
    ORDER BY r.started_at;
END;
$$;


ALTER FUNCTION "execution"."fn_poll_async_run"("p_stale_after_seconds" integer, "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_poll_async_run"("p_stale_after_seconds" integer, "p_limit" integer) IS 'Phase AM: SKIP LOCKED batch-claims pending async runs and bumps last_polled_at. Concurrent workers do not double-poll. Caller queries the provider status endpoint and calls fn_complete_async_run on success or fn_async_run_idempotent_complete to handle retries. service_role only.';



CREATE OR REPLACE FUNCTION "execution"."fn_record_execution_attestation"("p_run_id" "uuid", "p_device_id" "uuid" DEFAULT NULL::"uuid", "p_signed" boolean DEFAULT false, "p_signature" "text" DEFAULT NULL::"text", "p_gateway_verified" boolean DEFAULT false, "p_device_trusted" boolean DEFAULT false, "p_policy_passed" boolean DEFAULT false, "p_workflow_hash" "text" DEFAULT NULL::"text", "p_lens_hash" "text" DEFAULT NULL::"text", "p_agent_config_hash" "text" DEFAULT NULL::"text", "p_runner_version" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_attestation_id UUID;
BEGIN
  INSERT INTO execution.attestations (
    run_id, device_id, signed, signature,
    gateway_verified, device_trusted, policy_passed,
    workflow_hash, lens_hash, agent_config_hash,
    runner_version, cli_version
  ) VALUES (
    p_run_id, p_device_id, p_signed, p_signature,
    p_gateway_verified, p_device_trusted, p_policy_passed,
    p_workflow_hash, p_lens_hash, p_agent_config_hash,
    p_runner_version, p_cli_version
  )
  RETURNING id INTO v_attestation_id;

  RETURN v_attestation_id;
END;
$$;


ALTER FUNCTION "execution"."fn_record_execution_attestation"("p_run_id" "uuid", "p_device_id" "uuid", "p_signed" boolean, "p_signature" "text", "p_gateway_verified" boolean, "p_device_trusted" boolean, "p_policy_passed" boolean, "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text" DEFAULT NULL::"text", "p_lens_hash" "text" DEFAULT NULL::"text", "p_agent_config_hash" "text" DEFAULT NULL::"text", "p_runner_version" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_policy_passed" boolean DEFAULT false) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_attestation_id   UUID;
  v_canonical_bytes  BYTEA;
  v_signature_bytes  BYTEA;
  v_envelope_hash    TEXT;
  v_verify_row       RECORD;
  v_device_trusted   BOOLEAN := false;
BEGIN
  IF p_run_id IS NULL OR p_envelope_kid IS NULL THEN
    RAISE EXCEPTION 'invalid_input' USING ERRCODE = '22023';
  END IF;

  v_canonical_bytes := execution.fn_b64url_decode(p_canonical_jcs_b64url);
  v_signature_bytes := execution.fn_b64url_decode(p_signature_b64url);
  IF v_canonical_bytes IS NULL OR v_signature_bytes IS NULL THEN
    RAISE EXCEPTION 'invalid_envelope' USING ERRCODE = '22023';
  END IF;

  v_envelope_hash := encode(extensions.digest(v_canonical_bytes, 'sha256'), 'hex');

  SELECT (rd.trust_level IN ('approved','trusted'))
    INTO v_device_trusted
  FROM devices.registered_devices rd
  WHERE rd.id = p_envelope_kid;
  v_device_trusted := COALESCE(v_device_trusted, false);

  -- Insert the attestation row (append-only)
  INSERT INTO execution.attestations (
    run_id, device_id, signed, signature,
    gateway_verified, device_trusted, policy_passed,
    workflow_hash, lens_hash, agent_config_hash,
    runner_version, cli_version,
    envelope_kid, envelope_iat, envelope_nonce, envelope_body_jcs_sha256
  ) VALUES (
    p_run_id, p_envelope_kid, true, p_signature_b64url,
    false,                  -- gateway_verified is now derived from the verification row
    v_device_trusted, p_policy_passed,
    p_workflow_hash, p_lens_hash, p_agent_config_hash,
    p_runner_version, p_cli_version,
    p_envelope_kid, p_envelope_iat, p_envelope_nonce, v_envelope_hash
  )
  RETURNING id INTO v_attestation_id;

  -- Verify and persist verification result
  SELECT * INTO v_verify_row
  FROM execution.fn_verify_attestation_signature(
    p_envelope_kid, v_canonical_bytes, v_signature_bytes
  );

  INSERT INTO execution.attestation_verifications (
    attestation_id, verified, verified_at, invalid_reason,
    device_id, envelope_kid, envelope_iat, envelope_nonce, envelope_hash
  ) VALUES (
    v_attestation_id, COALESCE(v_verify_row.verified, false),
    now(), v_verify_row.invalid_reason,
    p_envelope_kid, p_envelope_kid, p_envelope_iat, p_envelope_nonce, v_envelope_hash
  );

  RETURN v_attestation_id;
END;
$$;


ALTER FUNCTION "execution"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) IS 'Atomic record+verify path for signed execution attestations (RFC-0003 Phase F). Verification result is always persisted to execution.attestation_verifications.';



CREATE OR REPLACE FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lenses', 'lensers', 'tenancy', 'public'
    AS $$
DECLARE
  v_lenser_id    uuid := "lensers"."get_auth_lenser_id"();
  v_workspace_id uuid;
  v_request_id   uuid;
  v_run_id       uuid;
  v_param_rec    RECORD;
  v_value        text;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Verify the lens exists and is accessible by the caller
  IF NOT EXISTS (
    SELECT 1 FROM "lenses"."lenses" l
    WHERE  l."id" = p_lens_id
      AND  (l."visibility" = 'public' OR l."lenser_id" = v_lenser_id)
  ) THEN
    RAISE EXCEPTION 'Lens % not found or access denied', p_lens_id
      USING ERRCODE = 'no_data_found';
  END IF;

  -- Validate inputs against version_parameters schema (DB-level enforcement)
  -- Raises check_violation if required params missing, types incompatible, or
  -- select values not in options. No-op if p_version_id IS NULL.
  PERFORM "lenses"."fn_validate_inputs"(p_version_id, p_inputs);

  -- Resolve workspace: fall back to lenser's personal workspace
  v_workspace_id := (
    SELECT "w"."id"
    FROM   "tenancy"."workspaces" w
    WHERE  "w"."owner_lenser_id" = v_lenser_id
      AND  "w"."type"            = 'personal'
      AND  "w"."status"          = 'active'
    ORDER  BY "w"."created_at" ASC
    LIMIT  1
  );

  -- Create execution request
  INSERT INTO "execution"."requests" (
    "requester_lenser_id",
    "lens_id",
    "version_id",
    "model_id",
    "input_snapshot",
    "funding_source",
    "byok_key_ref_id",
    "origin_type",
    "runtime_origin"
  ) VALUES (
    v_lenser_id,
    p_lens_id,
    p_version_id,
    p_model_id,
    p_inputs,
    p_funding_source,
    p_byok_key_id,
    'web',
    'cloud'
  )
  RETURNING "id" INTO v_request_id;

  -- Create execution run (queued → picked up by cloud worker)
  INSERT INTO "execution"."runs" (
    "request_id",
    "model_id",
    "status",
    "created_at"
  ) VALUES (
    v_request_id,
    p_model_id,
    'queued',
    now()
  )
  RETURNING "id" INTO v_run_id;

  -- Upsert used parameter values into lenses.version_parameter_contents so the
  -- user's last-used inputs are persisted per (parameter, workspace, lenser).
  -- Input keys are matched by parameter label (consistent with fn_create_lens).
  IF p_version_id IS NOT NULL AND v_workspace_id IS NOT NULL THEN
    FOR v_param_rec IN
      SELECT vp."id", vp."label" AS "key"
      FROM   "lenses"."version_parameters" vp
      WHERE  vp."version_id" = p_version_id
      LIMIT  200
    LOOP
      v_value := p_inputs->>v_param_rec."key";
      CONTINUE WHEN v_value IS NULL;

      INSERT INTO "lenses"."version_parameter_contents" (
        "parameter_id",
        "lenser_id",
        "workspace_id",
        "contents"
      ) VALUES (
        v_param_rec."id",
        v_lenser_id,
        v_workspace_id,
        jsonb_build_object('value', v_value)
      )
      ON CONFLICT ("parameter_id", "workspace_id", "lenser_id")
      DO UPDATE SET
        "contents"   = jsonb_build_object('value', v_value),
        "updated_at" = now();
    END LOOP;
  END IF;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid") IS 'SECURITY DEFINER: Creates execution.requests (with version_id) and execution.runs atomically. Validates p_inputs against lenses.version_parameters before creating the request — rejects missing required params, type mismatches, and invalid select values. Upserts supplied parameter values into lenses.version_parameter_contents so the user''s last-used inputs are persisted per (parameter, workspace, lenser). Returns the run_id. Caller must be authenticated. Lens must be public or owned by caller.';



CREATE OR REPLACE FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_id" "uuid" DEFAULT NULL::"uuid", "p_workspace_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lenses', 'lensers', 'tenancy', 'public'
    AS $$
DECLARE
  v_lenser_id    uuid := "lensers"."get_auth_lenser_id"();
  v_workspace_id uuid;
  v_request_id   uuid;
  v_run_id       uuid;
  v_param_rec    RECORD;
  v_value        text;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Verify the lens exists and is accessible by the caller
  IF NOT EXISTS (
    SELECT 1 FROM "lenses"."lenses" l
    WHERE  l."id" = p_lens_id
      AND  (l."visibility" = 'public' OR l."lenser_id" = v_lenser_id)
  ) THEN
    RAISE EXCEPTION 'Lens % not found or access denied', p_lens_id
      USING ERRCODE = 'no_data_found';
  END IF;

  -- Validate inputs against version_parameters schema
  -- (required presence, type compatibility, select enum membership)
  PERFORM "lenses"."fn_validate_inputs"(p_version_id, p_inputs);

  -- Resolve workspace: use provided value or fall back to lenser's personal workspace
  v_workspace_id := COALESCE(p_workspace_id, (
    SELECT "w"."id"
    FROM   "tenancy"."workspaces" w
    WHERE  "w"."owner_lenser_id" = v_lenser_id
      AND  "w"."type"            = 'personal'
      AND  "w"."status"          = 'active'
    ORDER  BY "w"."created_at" ASC
    LIMIT  1
  ));

  -- Create execution request
  INSERT INTO "execution"."requests" (
    "requester_lenser_id",
    "lens_id",
    "version_id",
    "model_id",
    "input_snapshot",
    "funding_source",
    "byok_key_ref_id",
    "workspace_id",
    "origin_type",
    "runtime_origin"
  ) VALUES (
    v_lenser_id,
    p_lens_id,
    p_version_id,
    p_model_id,
    p_inputs,
    p_funding_source,
    p_byok_key_id,
    v_workspace_id,
    'web',
    'cloud'
  )
  RETURNING "id" INTO v_request_id;

  -- Create execution run (queued → picked up by cloud worker)
  INSERT INTO "execution"."runs" (
    "request_id",
    "model_id",
    "status",
    "created_at"
  ) VALUES (
    v_request_id,
    p_model_id,
    'queued',
    now()
  )
  RETURNING "id" INTO v_run_id;

  -- Upsert used parameter values into lenses.version_parameter_contents.
  -- Input keys are matched by parameter label (consistent with fn_create_lens).
  IF p_version_id IS NOT NULL AND v_workspace_id IS NOT NULL THEN
    FOR v_param_rec IN
      SELECT vp."id", vp."label" AS "key"
      FROM   "lenses"."version_parameters" vp
      WHERE  vp."version_id" = p_version_id
      LIMIT  200
    LOOP
      v_value := p_inputs->>v_param_rec."key";
      CONTINUE WHEN v_value IS NULL;

      INSERT INTO "lenses"."version_parameter_contents" (
        "parameter_id",
        "lenser_id",
        "workspace_id",
        "contents"
      ) VALUES (
        v_param_rec."id",
        v_lenser_id,
        v_workspace_id,
        jsonb_build_object('value', v_value)
      )
      ON CONFLICT ("parameter_id", "workspace_id", "lenser_id")
      DO UPDATE SET
        "contents"   = jsonb_build_object('value', v_value),
        "updated_at" = now();
    END LOOP;
  END IF;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_workspace_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_workspace_id" "uuid") IS 'SECURITY DEFINER: Creates execution.requests (with version_id) and execution.runs atomically. Validates p_inputs against lenses.version_parameters before creating the request. Upserts supplied parameter values into lenses.version_parameter_contents. Returns the run_id. Caller must be authenticated. Lens must be public or owned by caller.';



CREATE OR REPLACE FUNCTION "execution"."fn_run_lens_api"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_id" "uuid" DEFAULT NULL::"uuid", "p_idempotency_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lenses', 'lensers', 'tenancy', 'public'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_workspace_id uuid;
  v_request_id uuid;
  v_run_id uuid;
  v_existing_run_id uuid;
  v_param_rec RECORD;
  v_value text;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF p_idempotency_key IS NOT NULL THEN
    SELECT r.id
      INTO v_existing_run_id
    FROM execution.requests req
    JOIN execution.runs r
      ON r.request_id = req.id
     AND r.is_active = true
    WHERE req.requester_lenser_id = v_lenser_id
      AND req.idempotency_key = p_idempotency_key
      AND req.lens_id = p_lens_id
      AND req.is_active = true
    ORDER BY req.created_at DESC
    LIMIT 1;

    IF v_existing_run_id IS NOT NULL THEN
      RETURN v_existing_run_id;
    END IF;
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM lenses.lenses l
    WHERE l.id = p_lens_id
      AND (l.visibility = 'public' OR l.lenser_id = v_lenser_id)
  ) THEN
    RAISE EXCEPTION 'Lens % not found or access denied', p_lens_id
      USING ERRCODE = 'no_data_found';
  END IF;

  PERFORM lenses.fn_validate_inputs(p_version_id, p_inputs);

  v_workspace_id := (
    SELECT w.id
    FROM tenancy.workspaces w
    WHERE w.owner_lenser_id = v_lenser_id
      AND w.type = 'personal'
      AND w.status = 'active'
    ORDER BY w.created_at ASC
    LIMIT 1
  );

  INSERT INTO execution.requests (
    requester_lenser_id,
    lens_id,
    version_id,
    model_id,
    input_snapshot,
    funding_source,
    byok_key_ref_id,
    workspace_id,
    origin_type,
    runtime_origin,
    idempotency_key
  ) VALUES (
    v_lenser_id,
    p_lens_id,
    p_version_id,
    p_model_id,
    p_inputs,
    p_funding_source,
    p_byok_key_id,
    v_workspace_id,
    'api',
    'cloud',
    p_idempotency_key
  )
  RETURNING id INTO v_request_id;

  INSERT INTO execution.runs (
    request_id,
    model_id,
    status,
    created_at
  ) VALUES (
    v_request_id,
    p_model_id,
    'queued',
    now()
  )
  RETURNING id INTO v_run_id;

  IF p_version_id IS NOT NULL AND v_workspace_id IS NOT NULL THEN
    FOR v_param_rec IN
      SELECT id, key
      FROM lenses.version_parameters
      WHERE version_id = p_version_id
    LOOP
      v_value := p_inputs->>v_param_rec.key;
      CONTINUE WHEN v_value IS NULL;

      INSERT INTO lenses.version_parameter_contents (
        parameter_id,
        lenser_id,
        workspace_id,
        contents
      ) VALUES (
        v_param_rec.id,
        v_lenser_id,
        v_workspace_id,
        jsonb_build_object('value', v_value)
      )
      ON CONFLICT (parameter_id, workspace_id, lenser_id)
      DO UPDATE SET
        contents = jsonb_build_object('value', v_value),
        updated_at = now();
    END LOOP;
  END IF;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_run_lens_api"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_binding_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required';
  END IF;

  -- Verify the device belongs to the caller
  IF NOT EXISTS (
    SELECT 1 FROM devices.registered_devices
    WHERE id = p_device_id
      AND lenser_id = v_lenser_id
      AND trust_level IN ('approved','trusted')
  ) THEN
    RAISE EXCEPTION 'device_not_found_or_not_trusted';
  END IF;

  INSERT INTO execution.runner_device_bindings (runner_id, device_id, status)
  VALUES (p_runner_id, p_device_id, 'active')
  ON CONFLICT (runner_id, device_id) DO UPDATE
    SET status = 'active', bound_at = now()
  RETURNING id INTO v_binding_id;

  RETURN v_binding_id;
END;
$$;


ALTER FUNCTION "execution"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_runner_list_with_devices"("p_limit" integer DEFAULT 50, "p_cursor" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("runner_id" "uuid", "device_id" "uuid", "device_name" "text", "trust_level" "text", "binding_status" "text", "bound_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required';
  END IF;

  p_limit := LEAST(p_limit, 100);

  RETURN QUERY
  SELECT
    b.runner_id,
    b.device_id,
    d.name  AS device_name,
    d.trust_level,
    b.status AS binding_status,
    b.bound_at
  FROM execution.runner_device_bindings b
  JOIN devices.registered_devices d ON d.id = b.device_id
  WHERE d.lenser_id = v_lenser_id
    AND (p_cursor IS NULL OR b.id > p_cursor)
  ORDER BY b.bound_at DESC
  LIMIT p_limit;
END;
$$;


ALTER FUNCTION "execution"."fn_runner_list_with_devices"("p_limit" integer, "p_cursor" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_set_artifact_visibility"("p_artifact_id" "uuid", "p_visibility" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id   uuid := "lensers"."get_auth_lenser_id"();
  v_run_status  text;
  v_owner_id    uuid;
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Validate visibility value
  IF p_visibility NOT IN ('private', 'public', 'contender_only', 'archived') THEN
    RAISE EXCEPTION 'Invalid visibility value: %', p_visibility
      USING ERRCODE = 'check_violation';
  END IF;

  -- Fetch run status + owner
  SELECT r."status", req."requester_lenser_id"
  INTO   v_run_status, v_owner_id
  FROM   "execution"."artifacts" a
  JOIN   "execution"."runs"     r   ON r."id"  = a."run_id"
  JOIN   "execution"."requests" req ON req."id" = r."request_id"
  WHERE  a."id" = p_artifact_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Artifact % not found', p_artifact_id
      USING ERRCODE = 'no_data_found';
  END IF;

  -- Ownership check
  IF v_owner_id <> v_caller_id THEN
    RAISE EXCEPTION 'Permission denied: you do not own this artifact'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Non-succeeded runs can only be set to 'private'
  IF v_run_status <> 'succeeded' AND p_visibility <> 'private' THEN
    RAISE EXCEPTION
      'Artifact from a % run can only be set to private visibility',
      v_run_status
      USING ERRCODE = 'check_violation';
  END IF;

  -- Apply
  UPDATE "execution"."artifacts"
  SET    "visibility" = p_visibility
  WHERE  "id" = p_artifact_id;
END;
$$;


ALTER FUNCTION "execution"."fn_set_artifact_visibility"("p_artifact_id" "uuid", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_set_artifact_visibility"("p_artifact_id" "uuid", "p_visibility" "text") IS 'SECURITY DEFINER: allows the artifact creator (owner) to change visibility. Validates: caller owns artifact via run→request→requester_lenser_id; non-succeeded run artifacts can only be private; valid visibility values only. Values: private (default), public (all users), contender_only, archived (owner only).';



CREATE OR REPLACE FUNCTION "execution"."fn_start_execution"("p_lenser_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_model_id" "uuid" DEFAULT NULL::"uuid", "p_lens_id" "uuid" DEFAULT NULL::"uuid", "p_workspace_id" "uuid" DEFAULT NULL::"uuid", "p_byok_key_ref_id" "uuid" DEFAULT NULL::"uuid", "p_input_snapshot" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("request_id" "uuid", "run_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'tenancy', 'public'
    AS $$
DECLARE
    v_request_id   "uuid";
    v_run_id       "uuid";
    v_workspace_id "uuid";
BEGIN
    -- Resolve workspace: caller may pass an explicit workspace_id (e.g. an org
    -- workspace), or NULL in which case we fall back to the lenser's personal
    -- workspace.  If that also doesn't exist the INSERT will error with a clear
    -- FK violation rather than silently using a wrong UUID.
    v_workspace_id := COALESCE(
        p_workspace_id,
        (
            SELECT w.id
            FROM   tenancy.workspaces w
            WHERE  w.owner_lenser_id = p_lenser_id
              AND  w.type            = 'personal'
              AND  w.status          = 'active'
            ORDER  BY w.created_at ASC
            LIMIT  1
        )
    );

    INSERT INTO "execution"."requests" (
        "requester_lenser_id",
        "origin_type",
        "funding_source",
        "model_id",
        "lens_id",
        "workspace_id",
        "byok_key_ref_id",
        "input_snapshot"
    ) VALUES (
        p_lenser_id,
        p_origin_type,
        p_funding_source,
        p_model_id,
        p_lens_id,
        v_workspace_id,
        p_byok_key_ref_id,
        p_input_snapshot
    )
    RETURNING "id" INTO v_request_id;

    INSERT INTO "execution"."runs" (
        "request_id",
        "status",
        "model_id",
        "started_at"
    ) VALUES (
        v_request_id,
        'running',
        p_model_id,
        now()
    )
    RETURNING "id" INTO v_run_id;

    RETURN QUERY SELECT v_request_id, v_run_id;
END;
$$;


ALTER FUNCTION "execution"."fn_start_execution"("p_lenser_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_model_id" "uuid", "p_lens_id" "uuid", "p_workspace_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_start_execution"("p_lenser_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_model_id" "uuid", "p_lens_id" "uuid", "p_workspace_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") IS 'Atomically creates execution.requests + execution.runs before the provider is called. Returns (request_id, run_id). Run is opened with status=running. When p_workspace_id is NULL, auto-resolves the lenser''s personal workspace (same pattern as execution.fn_run_lens). SECURITY DEFINER; callable only by service_role.';



CREATE OR REPLACE FUNCTION "execution"."fn_timeout_stale_runs"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  UPDATE execution.runs
  SET status = 'timed_out', completed_at = now()
  WHERE is_async = TRUE
    AND status = 'running'
    AND started_at < now() - INTERVAL '15 minutes';
  GET DIAGNOSTICS v_count = ROW_COUNT;
  RETURN v_count;
END;
$$;


ALTER FUNCTION "execution"."fn_timeout_stale_runs"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_verify_attestation_signature"("p_kid" "uuid", "p_canonical_jcs" "bytea", "p_signature" "bytea") RETURNS TABLE("verified" boolean, "invalid_reason" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'extensions', 'public'
    AS $_$
DECLARE
  v_pk_b64       TEXT;
  v_pk           BYTEA;
  v_pgsodium_ok  BOOLEAN;
  v_ok           BOOLEAN;
BEGIN
  IF p_kid IS NULL OR p_canonical_jcs IS NULL OR p_signature IS NULL THEN
    RETURN QUERY SELECT false, 'missing_input';
    RETURN;
  END IF;

  IF length(p_signature) <> 64 THEN
    RETURN QUERY SELECT false, 'bad_signature_length';
    RETURN;
  END IF;

  SELECT rd.public_key INTO v_pk_b64
  FROM devices.registered_devices rd
  WHERE rd.id = p_kid
    AND COALESCE(rd.signing_algo, 'ed25519') = 'ed25519';

  IF v_pk_b64 IS NULL THEN
    RETURN QUERY SELECT false, 'unknown_device_or_algo';
    RETURN;
  END IF;

  -- The keychain stores raw 32-byte Ed25519 public keys as base64url.
  v_pk := execution.fn_b64url_decode(v_pk_b64);
  IF v_pk IS NULL OR length(v_pk) <> 32 THEN
    -- Some clients may have submitted standard base64; try that as a fallback.
    BEGIN
      v_pk := decode(v_pk_b64, 'base64');
    EXCEPTION WHEN OTHERS THEN
      v_pk := NULL;
    END;
    IF v_pk IS NULL OR length(v_pk) <> 32 THEN
      RETURN QUERY SELECT false, 'malformed_public_key';
      RETURN;
    END IF;
  END IF;

  SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgsodium')
    INTO v_pgsodium_ok;
  IF NOT v_pgsodium_ok THEN
    RETURN QUERY SELECT false, 'pgsodium_unavailable';
    RETURN;
  END IF;

  BEGIN
    EXECUTE 'SELECT pgsodium.crypto_sign_verify_detached($1, $2, $3)'
      INTO v_ok
      USING p_signature, p_canonical_jcs, v_pk;
  EXCEPTION WHEN OTHERS THEN
    v_ok := NULL;
  END;

  IF v_ok IS TRUE THEN
    RETURN QUERY SELECT true, NULL::TEXT;
  ELSIF v_ok IS FALSE THEN
    RETURN QUERY SELECT false, 'signature_invalid';
  ELSE
    RETURN QUERY SELECT false, 'pgsodium_call_failed';
  END IF;
END;
$_$;


ALTER FUNCTION "execution"."fn_verify_attestation_signature"("p_kid" "uuid", "p_canonical_jcs" "bytea", "p_signature" "bytea") OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."fn_verify_attestation_signature"("p_kid" "uuid", "p_canonical_jcs" "bytea", "p_signature" "bytea") IS 'Ed25519 verification of an attestation envelope. Returns (verified, invalid_reason). Never grants verified=true silently if pgsodium is missing.';



CREATE OR REPLACE FUNCTION "execution"."fn_xp_apply_safe"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public', 'extensions'
    AS $$
BEGIN
  IF p_lenser_id IS NULL OR p_rule_key IS NULL THEN
    RETURN;
  END IF;
  BEGIN
    PERFORM xp.apply(
      p_lenser_id, p_rule_key, p_source,
      p_source_ref_type, p_source_ref_id, p_app_id
    );
  EXCEPTION WHEN OTHERS THEN
    RAISE NOTICE 'xp.apply failed for rule % on lenser %: %',
      p_rule_key, p_lenser_id, SQLERRM;
  END;
END;
$$;


ALTER FUNCTION "execution"."fn_xp_apply_safe"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_xp_on_full_trust"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'battles', 'lensers', 'xp', 'public', 'extensions'
    AS $$
DECLARE
  v_owner_lenser_id uuid;
  v_should_award    boolean := false;
BEGIN
  IF NEW.trust_level <> 'fully_trusted' THEN
    RETURN NEW;
  END IF;
  IF TG_OP = 'UPDATE' AND OLD.trust_level = 'fully_trusted' THEN
    -- already at full trust; do not re-award
    RETURN NEW;
  END IF;
  v_should_award := true;

  -- Owner = the run requester (and the submission belongs to them).
  SELECT req.requester_lenser_id INTO v_owner_lenser_id
  FROM battles.submissions s
  JOIN execution.runs r       ON r.id = s.execution_run_id
  JOIN execution.requests req ON req.id = r.request_id
  WHERE s.id = NEW.submission_id
  LIMIT 1;

  IF NOT v_should_award OR v_owner_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  PERFORM execution.fn_xp_apply_safe(
    v_owner_lenser_id,
    'VERIFIED_LOCAL_EXECUTION_COMPLETED',
    'battle'::xp.source_enum,
    'submission',
    NEW.submission_id,
    '00000000-0000-0000-0000-000000000002'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "execution"."fn_xp_on_full_trust"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_xp_on_runner_bound"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'devices', 'xp', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id uuid;
  v_trust_ok  boolean;
BEGIN
  IF COALESCE(NEW.status, 'inactive') <> 'active' THEN
    RETURN NEW;
  END IF;
  IF TG_OP = 'UPDATE'
     AND COALESCE(OLD.status, 'inactive') = 'active'
  THEN
    -- already active before; do not re-award
    RETURN NEW;
  END IF;

  SELECT rd.lenser_id, rd.trust_level IN ('approved', 'trusted')
    INTO v_lenser_id, v_trust_ok
  FROM devices.registered_devices rd
  WHERE rd.id = NEW.device_id;

  IF v_lenser_id IS NULL OR NOT COALESCE(v_trust_ok, false) THEN
    RETURN NEW;
  END IF;

  PERFORM execution.fn_xp_apply_safe(
    v_lenser_id,
    'RUNNER_CONNECTED',
    'system'::xp.source_enum,
    'runner',
    NEW.runner_id,
    '00000000-0000-0000-0000-000000000003'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "execution"."fn_xp_on_runner_bound"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."fn_xp_on_submission_evaluated"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'battles', 'lensers', 'xp', 'public', 'extensions'
    AS $$
DECLARE
  v_owner_lenser_id uuid;
BEGIN
  -- Only fire on initial INSERT; updates are re-evaluations of the same
  -- submission and would double-award.
  IF TG_OP <> 'INSERT' THEN
    RETURN NEW;
  END IF;
  IF NEW.trust_level = 'unverified' THEN
    RETURN NEW;
  END IF;

  SELECT req.requester_lenser_id INTO v_owner_lenser_id
  FROM battles.submissions s
  JOIN execution.runs r       ON r.id = s.execution_run_id
  JOIN execution.requests req ON req.id = r.request_id
  WHERE s.id = NEW.submission_id
  LIMIT 1;

  IF v_owner_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  PERFORM execution.fn_xp_apply_safe(
    v_owner_lenser_id,
    'BATTLE_SUBMISSION_COMPLETED',
    'battle'::xp.source_enum,
    'submission',
    NEW.submission_id,
    '00000000-0000-0000-0000-000000000002'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "execution"."fn_xp_on_submission_evaluated"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."insert_stream_run"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_model_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_input_snapshot" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_status" "text", "p_error_code" "text" DEFAULT NULL::"text", "p_bytes_sent" integer DEFAULT 0) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
DECLARE
    v_request_id uuid;
    v_billing_status text;
BEGIN
    -- Resolve billing_status from credit cost
    v_billing_status := CASE WHEN p_credit_cost > 0 THEN 'charged' ELSE 'free' END;

    -- Insert execution request row
    INSERT INTO execution.requests (
        requester_lenser_id,
        origin_type,
        model_id,
        input_snapshot,
        funding_source,
        runtime_origin
    ) VALUES (
        p_lenser_id,
        'api_stream',
        p_model_id,
        p_input_snapshot,
        'platform_credit',
        'cloud'
    )
    RETURNING id INTO v_request_id;

    -- Insert execution run row (id = p_run_id for idempotency via pending_charges FK)
    INSERT INTO execution.runs (
        id,
        request_id,
        status,
        model_id,
        token_input,
        token_output,
        credit_cost,
        billing_status,
        started_at,
        completed_at
    ) VALUES (
        p_run_id,
        v_request_id,
        p_status,
        p_model_id,
        p_token_input,
        p_token_output,
        p_credit_cost,
        v_billing_status,
        now(),
        CASE WHEN p_status IN ('succeeded', 'failed', 'timed_out') THEN now() ELSE NULL END
    )
    ON CONFLICT (id) DO UPDATE SET
        status          = EXCLUDED.status,
        token_input     = EXCLUDED.token_input,
        token_output    = EXCLUDED.token_output,
        credit_cost     = EXCLUDED.credit_cost,
        billing_status  = EXCLUDED.billing_status,
        completed_at    = EXCLUDED.completed_at;

    -- Upsert stream session
    INSERT INTO execution.stream_sessions (
        run_id,
        lenser_id,
        provider,
        model_key,
        status,
        token_input,
        token_output,
        credit_cost,
        error_code,
        bytes_sent,
        completed_at
    ) VALUES (
        p_run_id,
        p_lenser_id,
        p_provider_key,
        p_model_key,
        CASE p_status
            WHEN 'succeeded' THEN 'completed'
            WHEN 'failed'    THEN 'failed'
            WHEN 'timed_out' THEN 'timed_out'
            ELSE p_status
        END,
        p_token_input,
        p_token_output,
        p_credit_cost,
        p_error_code,
        p_bytes_sent,
        CASE WHEN p_status IN ('succeeded', 'failed', 'timed_out') THEN now() ELSE NULL END
    )
    ON CONFLICT (run_id) DO UPDATE SET
        status       = EXCLUDED.status,
        token_input  = EXCLUDED.token_input,
        token_output = EXCLUDED.token_output,
        credit_cost  = EXCLUDED.credit_cost,
        error_code   = EXCLUDED.error_code,
        bytes_sent   = EXCLUDED.bytes_sent,
        completed_at = EXCLUDED.completed_at;
END;
$$;


ALTER FUNCTION "execution"."insert_stream_run"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_model_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_input_snapshot" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_status" "text", "p_error_code" "text", "p_bytes_sent" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "execution"."insert_stream_run"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_model_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_input_snapshot" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_status" "text", "p_error_code" "text", "p_bytes_sent" integer) IS 'Atomically records a streaming AI execution run across execution.requests, execution.runs, and execution.stream_sessions. Called by the Cloudflare Worker after stream completion or failure.';



CREATE OR REPLACE FUNCTION "execution"."trg_requests_resolve_lens_snapshot"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'lenses', 'public', 'extensions'
    AS $$
DECLARE
  v_lens    lenses.lenses%ROWTYPE;
  v_version lenses.versions%ROWTYPE;
  v_snapshot JSONB;
BEGIN
  IF NEW.lens_id IS NULL THEN
    RETURN NEW;
  END IF;

  SELECT * INTO v_lens
  FROM lenses.lenses
  WHERE id = NEW.lens_id;

  IF NOT FOUND OR v_lens.archived_at IS NOT NULL OR v_lens.deleted_at IS NOT NULL THEN
    RAISE EXCEPTION 'lens_unavailable_for_execution: %', NEW.lens_id USING ERRCODE = '42501';
  END IF;

  IF NEW.version_id IS NULL THEN
    SELECT * INTO v_version
    FROM lenses.versions
    WHERE lens_id = NEW.lens_id
      AND status = 'published'::content.content_status
    ORDER BY version_number DESC
    LIMIT 1;

    IF NOT FOUND AND v_lens.head_version_id IS NOT NULL THEN
      SELECT * INTO v_version
      FROM lenses.versions
      WHERE id = v_lens.head_version_id;
    END IF;

    IF FOUND THEN
      NEW.version_id := v_version.id;
    END IF;
  ELSE
    SELECT * INTO v_version
    FROM lenses.versions
    WHERE id = NEW.version_id
      AND lens_id = NEW.lens_id;
  END IF;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'lens_version_unavailable_for_execution: %', NEW.lens_id USING ERRCODE = '42501';
  END IF;

  NEW.lens_content_hash := encode(v_version.content_hash, 'hex');
  v_snapshot := jsonb_build_object(
    'artifact_type', 'lens',
    'lens_id', NEW.lens_id,
    'version_id', v_version.id,
    'version_number', v_version.version_number,
    'content_hash', NEW.lens_content_hash,
    'snapshot_kind', 'lens_version'
  );

  NEW.resolved_artifact_snapshot := COALESCE(NEW.resolved_artifact_snapshot, v_snapshot);
  NEW.resolved_artifact_snapshot_hash := COALESCE(
    NEW.resolved_artifact_snapshot_hash,
    encode(extensions.digest(v_snapshot::text, 'sha256'), 'hex')
  );

  RETURN NEW;
END;
$$;


ALTER FUNCTION "execution"."trg_requests_resolve_lens_snapshot"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "execution"."trg_runs_set_completed_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'execution'
    AS $$
BEGIN
    IF NEW.status IN ('succeeded', 'failed', 'canceled', 'timed_out')
       AND OLD.status NOT IN ('succeeded', 'failed', 'canceled', 'timed_out')
       AND NEW.completed_at IS NULL THEN
        NEW.completed_at := NOW();
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "execution"."trg_runs_set_completed_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "i18n"."fn_is_supported_language"("p_code" "text") RETURNS boolean
    LANGUAGE "sql" IMMUTABLE PARALLEL SAFE
    SET "search_path" TO 'i18n', 'pg_catalog'
    AS $$
  SELECT p_code IS NULL OR p_code IN (
    'en','tr','es','fr','de','it','pt','pt-BR','nl','pl','ru','uk','cs',
    'sv','no','da','fi','hu','ro','el','ar','he','fa','hi','bn','ur',
    'zh','zh-CN','zh-TW','ja','ko','vi','th','id','ms','tl'
  );
$$;


ALTER FUNCTION "i18n"."fn_is_supported_language"("p_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "identity_gov"."fn_guard_lenser_handle"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_verdict    text;
  v_class      text;
  v_score      smallint;
  v_reasons    text[];
BEGIN
  IF current_setting('identity_gov.allow_reserved_seed', true) = 'on' THEN
    RETURN NEW;
  END IF;

  IF TG_OP = 'UPDATE' AND OLD.handle IS NOT DISTINCT FROM NEW.handle THEN
    RETURN NEW;
  END IF;

  SELECT verdict, class_hit, risk_score, reason_codes
  INTO   v_verdict, v_class, v_score, v_reasons
  FROM   identity_gov.fn_validate_handle(NEW.handle);

  IF v_verdict = 'deny' THEN
    RAISE EXCEPTION
      'Handle "%" is reserved or protected (class: %, score: %, reasons: %). Choose a different handle.',
      NEW.handle, coalesce(v_class, 'unknown'), v_score,
      array_to_string(v_reasons, ', ')
      USING ERRCODE = 'P0001';
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "identity_gov"."fn_guard_lenser_handle"() OWNER TO "postgres";


COMMENT ON FUNCTION "identity_gov"."fn_guard_lenser_handle"() IS 'BEFORE trigger on lensers.profiles. Delegates to fn_validate_handle. Raises P0001 on deny. Replaces the legacy lensers_reserved_handle_check constraint.';



CREATE OR REPLACE FUNCTION "identity_gov"."fn_handle_skeleton"("p_normalized" "text") RETURNS "text"
    LANGUAGE "sql" IMMUTABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  -- After fn_normalize_handle, most Latin/Cyrillic/Greek confusables are already
  -- folded. This function applies the remaining high-risk skeleton mappings:
  -- rn → m, vv → w, ii → u (common visual confusion in sans-serif fonts).
  SELECT regexp_replace(
    regexp_replace(
      regexp_replace(p_normalized, 'rn', 'm', 'g'),
    'vv', 'w', 'g'),
  'ii', 'u', 'g')
$$;


ALTER FUNCTION "identity_gov"."fn_handle_skeleton"("p_normalized" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "identity_gov"."fn_handle_skeleton"("p_normalized" "text") IS 'Applies font-level confusable folds on top of fn_normalize_handle output. Used by the homoglyph probe in fn_validate_handle.';



CREATE OR REPLACE FUNCTION "identity_gov"."fn_normalize_handle"("p_handle" "text") RETURNS "text"
    LANGUAGE "plpgsql" IMMUTABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $_$
DECLARE
  v text := p_handle;
  v2 text;
BEGIN
  IF v IS NULL OR v = '' THEN
    RETURN '';
  END IF;

  v := replace(v, chr(8203), '');
  v := replace(v, chr(8204), '');
  v := replace(v, chr(8205), '');
  v := replace(v, chr(65279), '');
  v := normalize(v, NFKC);
  v := lower(v);

  v := replace(v, chr(1072), 'a');
  v := replace(v, chr(1077), 'e');
  v := replace(v, chr(1086), 'o');
  v := replace(v, chr(1088), 'p');
  v := replace(v, chr(1089), 'c');
  v := replace(v, chr(1093), 'x');
  v := replace(v, chr(1080), 'i');

  v := translate(v, '013457@$', 'oleastas');
  v := replace(v, '8', 'b');

  LOOP
    v2 := regexp_replace(v, '(.)\1{2,}', '\1\1', 'g');
    EXIT WHEN v2 = v;
    v := v2;
  END LOOP;

  v := regexp_replace(v, '^the[_\.-]?', '', 'g');
  v := regexp_replace(v,
    '[_\.-]?(support|help|hq|team|real|inc|corp|llc|org|net|io|app|'
    'pro|plus|admin|mod|moderator|bot|system|sys|core|api|dev|central|'
    'global|world|portal|hub|connect|verify)$',
    '', 'g');
  v := trim(both '_-.' FROM v);

  RETURN v;
END;
$_$;


ALTER FUNCTION "identity_gov"."fn_normalize_handle"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "identity_gov"."fn_normalize_handle"("p_handle" "text") IS 'Pure normalizer. NFKC → casefold → homoglyph fold → leet fold → repeat collapse → filler suffix strip. Returns empty string on NULL/empty input. IMMUTABLE — result depends only on input, safe to index.';



CREATE OR REPLACE FUNCTION "identity_gov"."fn_validate_handle"("p_candidate" "text") RETURNS TABLE("verdict" "text", "class_hit" "text", "risk_score" smallint, "reason_codes" "text"[])
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'identity_gov', 'extensions', 'pg_catalog'
    AS $_$
DECLARE
  v_norm        text;
  v_skeleton    text;
  v_metaphone   text;
  v_score       smallint := 0;
  v_class       text     := null;
  v_reasons     text[]   := '{}';
  v_deny_floor  smallint;

  v_modifiers   text[] := ARRAY[
    'ai','bot','official','admin','mod','moderator','support','help',
    'hq','team','real','inc','corp','llc','sys','system','core','api',
    'dev','io','app','pro','plus','central','global','hub','connect',
    'verify','verified','iam','portal','world','gpt','llm'
  ];
  r             record;
BEGIN
  -- ── [1] Syntax guard ────────────────────────────────────────────────────
  IF p_candidate IS NULL OR length(trim(p_candidate)) < 3 THEN
    RETURN QUERY SELECT 'deny'::text, 'system'::text, 100::smallint, ARRAY['syntax_too_short'];
    RETURN;
  END IF;
  IF length(p_candidate) > 30 THEN
    RETURN QUERY SELECT 'deny'::text, 'system'::text, 100::smallint, ARRAY['syntax_too_long'];
    RETURN;
  END IF;
  IF p_candidate !~ '^[a-zA-Z0-9_\-]+$' THEN
    RETURN QUERY SELECT 'deny'::text, 'system'::text, 100::smallint, ARRAY['syntax_invalid_chars'];
    RETURN;
  END IF;

  -- ── [2] Normalize ───────────────────────────────────────────────────────
  v_norm      := "identity_gov"."fn_normalize_handle"(p_candidate);
  v_skeleton  := "identity_gov"."fn_handle_skeleton"(v_norm);
  v_metaphone := "extensions"."dmetaphone"(v_norm);

  IF length(v_norm) < 2 THEN
    RETURN QUERY SELECT 'deny'::text, 'security'::text, 95::smallint, ARRAY['evasion_normalized_too_short'];
    RETURN;
  END IF;

  -- ── [3] Exact registry hit ──────────────────────────────────────────────
  SELECT "deny_score", "class"
  INTO   v_deny_floor, v_class
  FROM   "identity_gov"."reserved_namespaces"
  WHERE  "entry_kind" = 'exact'
    AND  "value"      = v_norm
    AND  ("expires_at" IS NULL OR "expires_at" > now())
  LIMIT 1;

  IF FOUND THEN
    RETURN QUERY SELECT 'deny'::text, v_class, v_deny_floor, ARRAY['exact_match'];
    RETURN;
  END IF;

  -- ── [4] Prefix / suffix / regex pattern match ───────────────────────────
  FOR r IN
    SELECT "entry_kind", "value", "class", "deny_score"
    FROM   "identity_gov"."reserved_namespaces"
    WHERE  "entry_kind" IN ('prefix', 'suffix', 'regex')
      AND  ("expires_at" IS NULL OR "expires_at" > now())
  LOOP
    IF r.entry_kind = 'prefix' AND left(v_norm, length(r.value)) = r.value THEN
      RETURN QUERY SELECT 'deny'::text, r.class, r.deny_score, ARRAY['prefix_match'];
      RETURN;
    END IF;
    IF r.entry_kind = 'suffix' AND right(v_norm, length(r.value)) = r.value THEN
      v_score := greatest(v_score, 60::smallint);
      v_class := coalesce(v_class, r.class);
      v_reasons := array_append(v_reasons, 'suffix_match');
    END IF;
    IF r.entry_kind = 'regex' AND v_norm ~ r.value THEN
      RETURN QUERY SELECT 'deny'::text, r.class, r.deny_score, ARRAY['regex_match'];
      RETURN;
    END IF;
  END LOOP;

  -- ── [5] Token + modifier composition check ──────────────────────────────
  FOR r IN
    SELECT "value", "class", "deny_score"
    FROM   "identity_gov"."reserved_namespaces"
    WHERE  "entry_kind" = 'token'
      AND  ("expires_at" IS NULL OR "expires_at" > now())
  LOOP
    IF v_norm LIKE ('%' || r.value || '%') THEN
      IF EXISTS (
        SELECT 1
        FROM   unnest(v_modifiers) AS m(mod)
        WHERE  lower(p_candidate) LIKE ('%' || m.mod || '%')
           OR  lower(p_candidate) LIKE (m.mod || '%')
           OR  lower(p_candidate) LIKE ('%' || m.mod)
      ) THEN
        RETURN QUERY SELECT 'deny'::text, r.class, r.deny_score, ARRAY['composition_token_modifier'];
        RETURN;
      END IF;
      v_score := greatest(v_score, 45::smallint);
      v_class := coalesce(v_class, r.class);
      v_reasons := array_append(v_reasons, 'token_match');
    END IF;
  END LOOP;

  -- ── [6] Homoglyph skeleton probe ─────────────────────────────────────────
  -- Uses reserved_namespaces_skeleton_idx (btree on fn_handle_skeleton(value)).
  SELECT "deny_score", "class"
  INTO   v_deny_floor, r.class
  FROM   "identity_gov"."reserved_namespaces"
  WHERE  "entry_kind" = 'exact'
    AND  "identity_gov"."fn_handle_skeleton"("value") = v_skeleton
    AND  ("expires_at" IS NULL OR "expires_at" > now())
  LIMIT 1;

  IF FOUND THEN
    RETURN QUERY SELECT 'deny'::text, r.class, v_deny_floor, ARRAY['homoglyph_skeleton'];
    RETURN;
  END IF;

  -- ── [7] Levenshtein fuzzy probe (edit distance ≤ 2) ──────────────────────
  -- BETWEEN lets the planner use reserved_namespaces_exact_len_idx for a range
  -- seek, skipping rows whose length differs by more than 2 from the candidate.
  FOR r IN
    SELECT "value", "class", "deny_score"
    FROM   "identity_gov"."reserved_namespaces"
    WHERE  "entry_kind" = 'exact'
      AND  length("value") BETWEEN length(v_norm) - 2 AND length(v_norm) + 2
      AND  ("expires_at" IS NULL OR "expires_at" > now())
  LOOP
    IF "extensions"."levenshtein_less_equal"(v_norm, r.value, 2) <= 2 THEN
      IF "extensions"."levenshtein"(v_norm, r.value) <= 1 THEN
        RETURN QUERY SELECT 'deny'::text, r.class, r.deny_score, ARRAY['levenshtein_distance_1'];
        RETURN;
      ELSE
        v_score := greatest(v_score, 55::smallint);
        v_class := coalesce(v_class, r.class);
        v_reasons := array_append(v_reasons, 'levenshtein_distance_2');
      END IF;
    END IF;
  END LOOP;

  -- ── [8] Trigram similarity probe ─────────────────────────────────────────
  -- Uses reserved_namespaces_trgm_idx (GIN gin_trgm_ops WHERE entry_kind='exact').
  -- The '%' operator and extensions.similarity() both live in the extensions
  -- schema; they resolve correctly only when 'extensions' is in search_path.
  SELECT "class", "deny_score"
  INTO   v_class, v_deny_floor
  FROM   "identity_gov"."reserved_namespaces"
  WHERE  "entry_kind" = 'exact'
    AND  ("expires_at" IS NULL OR "expires_at" > now())
    AND  "value" % v_norm
    AND  "extensions"."similarity"("value", v_norm) > 0.70
  ORDER BY "extensions"."similarity"("value", v_norm) DESC
  LIMIT 1;

  IF FOUND THEN
    v_score := greatest(v_score, 50::smallint);
    v_reasons := array_append(v_reasons, 'trigram_similarity');
  END IF;

  -- ── [9] Phonetic probe (Double-Metaphone) ────────────────────────────────
  -- Uses reserved_namespaces_metaphone_idx (btree on dmetaphone(value)).
  IF v_metaphone IS NOT NULL AND length(v_metaphone) > 0 THEN
    SELECT "class", "deny_score"
    INTO   v_class, v_deny_floor
    FROM   "identity_gov"."reserved_namespaces"
    WHERE  "entry_kind" = 'exact'
      AND  "extensions"."dmetaphone"("value") = v_metaphone
      AND  ("expires_at" IS NULL OR "expires_at" > now())
    LIMIT 1;

    IF FOUND THEN
      v_score := greatest(v_score, 40::smallint);
      v_reasons := array_append(v_reasons, 'phonetic_match');
    END IF;
  END IF;

  -- ── Final verdict ────────────────────────────────────────────────────────
  IF v_score >= 70 THEN
    RETURN QUERY SELECT 'deny'::text, v_class, v_score, v_reasons;
  ELSIF v_score >= 40 THEN
    RETURN QUERY SELECT 'escalate'::text, v_class, v_score, v_reasons;
  ELSE
    RETURN QUERY SELECT 'allow'::text, null::text, v_score, v_reasons;
  END IF;

EXCEPTION WHEN OTHERS THEN
  RETURN QUERY SELECT 'deny'::text, 'security'::text, 90::smallint,
    ARRAY['internal_error:' || SQLERRM];
END;
$_$;


ALTER FUNCTION "identity_gov"."fn_validate_handle"("p_candidate" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "identity_gov"."fn_validate_handle"("p_candidate" "text") IS '9-step Chain of Responsibility validation pipeline. Returns verdict, class_hit, risk_score, reason_codes. Fails CLOSED on exception. STABLE — reads registry rows but writes nothing. Step 7 uses BETWEEN (not abs) so reserved_namespaces_exact_len_idx is usable.';



CREATE OR REPLACE FUNCTION "internal"."get_service_role_key"() RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_key text;
BEGIN
  -- Prefer Vault (operator inserts: INSERT INTO vault.secrets(name,secret)
  -- VALUES ('service_role_key','<key>'))
  BEGIN
    SELECT decrypted_secret INTO v_key
    FROM vault.decrypted_secrets
    WHERE name = 'service_role_key'
    LIMIT 1;
  EXCEPTION WHEN undefined_table OR insufficient_privilege THEN
    v_key := NULL;
  END;

  -- Fallback to GUC during migration period
  IF v_key IS NULL OR v_key = '' THEN
    v_key := current_setting('app.service_role_key', true);
  END IF;

  RETURN v_key;
END;
$$;


ALTER FUNCTION "internal"."get_service_role_key"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."anonymize_join_log"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$BEGIN
  UPDATE analytics.lenser_join_log
  SET lenser_id = NULL
  WHERE lenser_id = OLD.id;
  RETURN OLD;
END;$$;


ALTER FUNCTION "lensers"."anonymize_join_log"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."assign_country_join_order"("p_lenser_id" "uuid", "p_country_code" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
BEGIN
    -- no-op: analytics.lenser_country_join_log has been retired
    NULL;
END;
$$;


ALTER FUNCTION "lensers"."assign_country_join_order"("p_lenser_id" "uuid", "p_country_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."auto_award_badges_from_join_order"("p_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$DECLARE
  v_join_order bigint;
BEGIN
  -- lenser_join_log içinden global join_order çek
  SELECT join_order
  INTO v_join_order
  FROM analytics.lenser_join_log
  WHERE lenser_id = p_lenser_id;

  IF v_join_order IS NULL THEN
    RETURN;
  END IF;

  -- İlk 10: FOUNDING_10
  IF v_join_order <= 10 THEN
    PERFORM lensers.award_badge(
      p_lenser_id,
      'FOUNDING_10',
      'Founding Member (Top 10)',
      'You are among the first 10 Lensers!'
    );
  END IF;

  -- İlk 100: FOUNDING_100
  IF v_join_order <= 100 THEN
    PERFORM lensers.award_badge(
      p_lenser_id,
      'FOUNDING_100',
      'Founding Member (Top 100)',
      'You are among the first 100 Lensers!'
    );
  END IF;

  -- İlk 1000: FOUNDING_1000
  IF v_join_order <= 1000 THEN
    PERFORM lensers.award_badge(
      p_lenser_id,
      'FOUNDING_1000',
      'Founding Member (Top 1000)',
      'You are among the first 1000 Lensers!'
    );
  END IF;
END;$$;


ALTER FUNCTION "lensers"."auto_award_badges_from_join_order"("p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."auto_award_badges_from_level"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_old_level" integer, "p_new_level" integer, "p_event_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
BEGIN
  IF p_old_level < 5 AND p_new_level >= 5 THEN
    PERFORM award_badge(p_lenser_id, 'LEVEL_5',
      'Level 5 Achieved', 'Reached Level 5', NULL, p_event_id);
  END IF;

  IF p_old_level < 10 AND p_new_level >= 10 THEN
    PERFORM award_badge(p_lenser_id, 'LEVEL_10',
      'Level 10 Achieved', 'Reached Level 10', NULL, p_event_id);
  END IF;

  IF p_old_level < 20 AND p_new_level >= 20 THEN
    PERFORM award_badge(p_lenser_id, 'LEVEL_20',
      'Level 20 Achieved', 'Reached Level 20', NULL, p_event_id);
  END IF;

  IF p_old_level < 30 AND p_new_level >= 30 THEN
    PERFORM award_badge(p_lenser_id, 'LEVEL_30',
      'Level 30 Achieved', 'Reached Level 30', NULL, p_event_id);
  END IF;
END;
$$;


ALTER FUNCTION "lensers"."auto_award_badges_from_level"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_old_level" integer, "p_new_level" integer, "p_event_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."auto_award_badges_from_streak"("p_lenser_id" "uuid", "p_streak" integer, "p_event_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
BEGIN
  IF p_streak = 7 THEN
    PERFORM award_badge(p_lenser_id, 'STREAK_7',
      '7-Day Streak', 'Maintained a 7-day XP streak', NULL, p_event_id);
  END IF;

  IF p_streak = 30 THEN
    PERFORM award_badge(p_lenser_id, 'STREAK_30',
      '30-Day Streak', 'Maintained a 30-day XP streak', NULL, p_event_id);
  END IF;

  IF p_streak = 100 THEN
    PERFORM award_badge(p_lenser_id, 'STREAK_100',
      '100-Day Streak', 'Maintained a 100-day XP streak', NULL, p_event_id);
  END IF;
END;
$$;


ALTER FUNCTION "lensers"."auto_award_badges_from_streak"("p_lenser_id" "uuid", "p_streak" integer, "p_event_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."award_badge"("p_lenser_id" "uuid", "p_type" "lensers"."lenser_badge_type", "p_label" "text", "p_description" "text" DEFAULT NULL::"text", "p_icon" "text" DEFAULT NULL::"text", "p_xp_event_id" "uuid" DEFAULT NULL::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$BEGIN
  -- If badge already exists, skip (no duplicates)
  IF EXISTS (
    SELECT 1 FROM lensers.badges
    WHERE lenser_id = p_lenser_id
      AND type = p_type
  ) THEN
    RETURN;
  END IF;

  INSERT INTO lensers.badges (
    lenser_id, type, category, label, description, icon, xp_event_id
  )
  VALUES (
    p_lenser_id,
    p_type,
    'achievement',
    p_label,
    p_description,
    p_icon,
    p_xp_event_id
  );
END;$$;


ALTER FUNCTION "lensers"."award_badge"("p_lenser_id" "uuid", "p_type" "lensers"."lenser_badge_type", "p_label" "text", "p_description" "text", "p_icon" "text", "p_xp_event_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."build_author_profile"("p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'content', 'xp', 'public', 'auth'
    AS $$DECLARE
    l_record record;
BEGIN
    SELECT handle, display_name, avatar_url
    INTO l_record
    FROM lensers.profiles
    WHERE id = p_lenser_id;

    RETURN jsonb_build_object(
        'handle', l_record.handle,
        'display_name', l_record.display_name,
        'avatar_url', l_record.avatar_url
    );
END;$$;


ALTER FUNCTION "lensers"."build_author_profile"("p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."current_active_lenser_id"() RETURNS "uuid"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$SELECT lensers.get_auth_lenser_id();$$;


ALTER FUNCTION "lensers"."current_active_lenser_id"() OWNER TO "postgres";


COMMENT ON FUNCTION "lensers"."current_active_lenser_id"() IS 'Returns the currently active lenser workspace id (human or owned AI), delegating to lensers.get_auth_lenser_id().';



CREATE OR REPLACE FUNCTION "lensers"."delete_expired_lensers"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'lenses', 'analytics', 'content'
    AS $$
DECLARE
  rec RECORD;
BEGIN
  FOR rec IN
    SELECT id
    FROM lensers.profiles
    WHERE deletion_requested_at IS NOT NULL
      AND deletion_requested_at <= now() - INTERVAL '5 minutes'
  LOOP
    DELETE FROM content.reactions   WHERE lenser_id = rec.id;
    DELETE FROM content.thread_replies WHERE lenser_id = rec.id;
    DELETE FROM content.threads     WHERE lenser_id = rec.id;
    DELETE FROM lenses.lenses       WHERE lenser_id = rec.id;
    DELETE FROM lensers.social_links WHERE lenser_id = rec.id;
    DELETE FROM lensers.profiles    WHERE id = rec.id;
  END LOOP;
END;
$$;


ALTER FUNCTION "lensers"."delete_expired_lensers"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."enforce_lensers_protections"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
DECLARE
  v_role text := current_setting('request.jwt.claim.role', true);
BEGIN
  IF TG_OP = 'INSERT' THEN
    -- Client insert'lerinde user_id zorunlu olarak auth.uid()
    IF v_role <> 'service_role' THEN
      IF auth.uid() IS NULL THEN
        RAISE EXCEPTION 'Only authenticated users can create a Lenser profile.';
      END IF;

      NEW.user_id := auth.uid();
      NEW.created_at := now();
      NEW.updated_at := now();
      NEW.handle := lower(NEW.handle);
      NEW.last_handle_changed_at := now();
    END IF;

    RETURN NEW;
  END IF;

  IF TG_OP = 'UPDATE' THEN
    IF v_role <> 'service_role' THEN
      -- user_id, created_at, engagement, status user tarafından değiştirilemez
      NEW.user_id := OLD.user_id;
      NEW.created_at := OLD.created_at;
      NEW.engagement := OLD.engagement;
      NEW.status := OLD.status;

      -- handle değişiyorsa 14 gün kuralı
      IF NEW.handle IS DISTINCT FROM OLD.handle THEN
        IF (now() - COALESCE(OLD.last_handle_changed_at, OLD.created_at)) < interval '14 days' THEN
          RAISE EXCEPTION 'Handle can only be changed once every 14 days.';
        END IF;

        NEW.handle := lower(NEW.handle);
        NEW.last_handle_changed_at := now();
      END IF;

      -- deletion_requested_at sadece ileriye dönük set edilebilir, geriye alınamaz
      IF NEW.deletion_requested_at IS DISTINCT FROM OLD.deletion_requested_at THEN
        IF OLD.deletion_requested_at IS NOT NULL THEN
          RAISE EXCEPTION 'Deletion request cannot be reverted or modified by the user.';
        END IF;
        NEW.deletion_requested_at := now();
      END IF;
    END IF;

    -- updated_at her zaman now()
    NEW.updated_at := now();
    RETURN NEW;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."enforce_lensers_protections"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."fn_can_view_profile"("p_viewer_auth_uid" "uuid", "p_subject_profile_id" "uuid") RETURNS "lensers"."profile_access_level"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
DECLARE
  v_subject RECORD;
  v_viewer_profile_id uuid;
  v_is_owner boolean := false;
  v_rel_state jsonb;
  v_is_blocked boolean;
  v_viewer_to_subject text;
BEGIN
  -- Fetch subject profile
  SELECT p.id, p.user_id, p.status, p.visibility, p.deletion_requested_at
  INTO v_subject
  FROM lensers.profiles p
  WHERE p.id = p_subject_profile_id;

  IF NOT FOUND THEN
    RETURN 'UNAVAILABLE_PROFILE'::lensers.profile_access_level;
  END IF;

  -- Deleted accounts → always unavailable
  IF v_subject.status = 'deleted' THEN
    RETURN 'UNAVAILABLE_PROFILE'::lensers.profile_access_level;
  END IF;

  -- Check ownership
  IF p_viewer_auth_uid IS NOT NULL AND v_subject.user_id = p_viewer_auth_uid THEN
    v_is_owner := true;
  END IF;

  -- Deactivated / pending_deletion
  IF v_subject.status IN ('deactivated', 'pending_deletion') THEN
    IF v_is_owner THEN
      RETURN 'OWNER_RECOVERY_PROFILE'::lensers.profile_access_level;
    ELSE
      RETURN 'UNAVAILABLE_PROFILE'::lensers.profile_access_level;
    END IF;
  END IF;

  -- Suspended
  IF v_subject.status = 'suspended' THEN
    RETURN 'UNAVAILABLE_PROFILE'::lensers.profile_access_level;
  END IF;

  -- Get viewer profile id
  IF p_viewer_auth_uid IS NOT NULL AND NOT v_is_owner THEN
    SELECT p.id INTO v_viewer_profile_id
    FROM lensers.profiles p
    WHERE p.user_id = p_viewer_auth_uid;
  END IF;

  -- Check block status
  IF v_viewer_profile_id IS NOT NULL THEN
    v_rel_state := lensers.fn_relationship_state(v_viewer_profile_id, v_subject.id);
    v_is_blocked := (v_rel_state->>'is_blocked')::boolean;

    IF v_is_blocked THEN
      RETURN 'UNAVAILABLE_PROFILE'::lensers.profile_access_level;
    END IF;
  END IF;

  -- Public / community → full access
  IF v_subject.visibility IN ('public', 'community') THEN
    RETURN 'FULL_PROFILE'::lensers.profile_access_level;
  END IF;

  -- Private profile logic
  IF v_subject.visibility = 'private' THEN
    IF v_is_owner THEN
      RETURN 'FULL_PROFILE'::lensers.profile_access_level;
    END IF;

    -- Check accepted follow relationship
    IF v_viewer_profile_id IS NOT NULL THEN
      v_viewer_to_subject := v_rel_state->>'viewer_to_subject';
      IF v_viewer_to_subject = 'accepted' THEN
        RETURN 'FULL_PROFILE'::lensers.profile_access_level;
      END IF;
    END IF;

    RETURN 'RESTRICTED_PROFILE'::lensers.profile_access_level;
  END IF;

  -- Fallback
  RETURN 'FULL_PROFILE'::lensers.profile_access_level;
END;
$$;


ALTER FUNCTION "lensers"."fn_can_view_profile"("p_viewer_auth_uid" "uuid", "p_subject_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."fn_create_default_preferences"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
BEGIN
    INSERT INTO lensers.preferences (lenser_id)
    VALUES (NEW.id)
    ON CONFLICT (lenser_id) DO NOTHING;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."fn_create_default_preferences"() OWNER TO "postgres";


COMMENT ON FUNCTION "lensers"."fn_create_default_preferences"() IS 'Trigger function: creates a default preferences row for every new lenser profile. SECURITY DEFINER to bypass RLS on insert.';



CREATE OR REPLACE FUNCTION "lensers"."fn_prevent_preferences_delete"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
BEGIN
    -- Allow deletion only when the parent profile is already gone (cascade path).
    -- If the profile still exists, the caller is trying to delete preferences directly.
    IF EXISTS (SELECT 1 FROM lensers.profiles WHERE id = OLD.lenser_id) THEN
        RAISE EXCEPTION
            'Cannot delete lensers.preferences directly for lenser %. '
            'Delete the lensers.profiles row instead — preferences will cascade.',
            OLD.lenser_id
        USING ERRCODE = 'P0001';
    END IF;
    RETURN OLD;
END;
$$;


ALTER FUNCTION "lensers"."fn_prevent_preferences_delete"() OWNER TO "postgres";


COMMENT ON FUNCTION "lensers"."fn_prevent_preferences_delete"() IS 'BEFORE DELETE guard on lensers.preferences. Blocks direct deletion when the parent profile still exists. Cascade deletes from lensers.profiles are still allowed (profile gone before trigger checks).';



CREATE OR REPLACE FUNCTION "lensers"."fn_prevent_self_relationship"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers'
    AS $$
BEGIN
    IF NEW.source_profile_id = NEW.target_profile_id THEN
        RAISE EXCEPTION 'A lenser cannot follow themselves'
            USING ERRCODE = '23514';
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."fn_prevent_self_relationship"() OWNER TO "postgres";


COMMENT ON FUNCTION "lensers"."fn_prevent_self_relationship"() IS 'BEFORE trigger guard: rejects any INSERT or UPDATE that would create a self-referential relationship (source_profile_id = target_profile_id). Raises SQLSTATE 23514 (check_violation) for consistent client handling. Belt-and-suspenders alongside the chk_no_self_rel CHECK constraint. Added in migration 000056.';



CREATE OR REPLACE FUNCTION "lensers"."fn_relationship_state"("p_viewer_id" "uuid", "p_subject_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE
    SET "search_path" TO 'lensers', 'public'
    AS $$
DECLARE
  v_viewer_to_subject lensers.relationship_status;
  v_subject_to_viewer lensers.relationship_status;
  v_is_mutual boolean := false;
  v_is_blocked boolean := false;
  v_is_close_circle boolean := false;
BEGIN
  IF p_viewer_id IS NULL OR p_subject_id IS NULL OR p_viewer_id = p_subject_id THEN
    RETURN jsonb_build_object(
      'viewer_to_subject', null,
      'subject_to_viewer', null,
      'is_mutual', false,
      'is_blocked', false,
      'is_close_circle', false
    );
  END IF;

  SELECT r.status, r.is_close_circle INTO v_viewer_to_subject, v_is_close_circle
  FROM lensers.relationships r
  WHERE r.source_profile_id = p_viewer_id AND r.target_profile_id = p_subject_id
    AND r.status IN ('pending', 'accepted', 'blocked');

  SELECT r.status INTO v_subject_to_viewer
  FROM lensers.relationships r
  WHERE r.source_profile_id = p_subject_id AND r.target_profile_id = p_viewer_id
    AND r.status IN ('pending', 'accepted', 'blocked');

  v_is_blocked := (v_viewer_to_subject = 'blocked') OR (v_subject_to_viewer = 'blocked');
  v_is_mutual := (v_viewer_to_subject = 'accepted') AND (v_subject_to_viewer = 'accepted');

  RETURN jsonb_build_object(
    'viewer_to_subject', v_viewer_to_subject,
    'subject_to_viewer', v_subject_to_viewer,
    'is_mutual', v_is_mutual,
    'is_blocked', v_is_blocked,
    'is_close_circle', v_is_close_circle
  );
END;
$$;


ALTER FUNCTION "lensers"."fn_relationship_state"("p_viewer_id" "uuid", "p_subject_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."fn_sync_relationship_counts"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'analytics'
    AS $$
DECLARE
  v_is_mutual boolean;
BEGIN
  -- Handle INSERT
  IF TG_OP = 'INSERT' AND NEW.status = 'accepted' THEN
    INSERT INTO analytics.lenser_stats (lenser_id, follower_count)
      VALUES (NEW.target_profile_id, 1)
      ON CONFLICT (lenser_id) DO UPDATE
        SET follower_count = analytics.lenser_stats.follower_count + 1, updated_at = now();

    INSERT INTO analytics.lenser_stats (lenser_id, following_count)
      VALUES (NEW.source_profile_id, 1)
      ON CONFLICT (lenser_id) DO UPDATE
        SET following_count = analytics.lenser_stats.following_count + 1, updated_at = now();

    -- Check mutual
    SELECT EXISTS (
      SELECT 1 FROM lensers.relationships
      WHERE source_profile_id = NEW.target_profile_id
        AND target_profile_id = NEW.source_profile_id
        AND status = 'accepted'
    ) INTO v_is_mutual;

    IF v_is_mutual THEN
      UPDATE analytics.lenser_stats
        SET mutuals_count = mutuals_count + 1, updated_at = now()
        WHERE lenser_id IN (NEW.source_profile_id, NEW.target_profile_id);
    END IF;

  -- Handle UPDATE (status change)
  ELSIF TG_OP = 'UPDATE' THEN
    -- Became accepted
    IF OLD.status <> 'accepted' AND NEW.status = 'accepted' THEN
      UPDATE analytics.lenser_stats
        SET follower_count = follower_count + 1, updated_at = now()
        WHERE lenser_id = NEW.target_profile_id;
      UPDATE analytics.lenser_stats
        SET following_count = following_count + 1, updated_at = now()
        WHERE lenser_id = NEW.source_profile_id;

      SELECT EXISTS (
        SELECT 1 FROM lensers.relationships
        WHERE source_profile_id = NEW.target_profile_id
          AND target_profile_id = NEW.source_profile_id
          AND status = 'accepted'
      ) INTO v_is_mutual;

      IF v_is_mutual THEN
        UPDATE analytics.lenser_stats
          SET mutuals_count = mutuals_count + 1, updated_at = now()
          WHERE lenser_id IN (NEW.source_profile_id, NEW.target_profile_id);
      END IF;
    END IF;

    -- Was accepted, now not
    IF OLD.status = 'accepted' AND NEW.status <> 'accepted' THEN
      UPDATE analytics.lenser_stats
        SET follower_count = GREATEST(0, follower_count - 1), updated_at = now()
        WHERE lenser_id = NEW.target_profile_id;
      UPDATE analytics.lenser_stats
        SET following_count = GREATEST(0, following_count - 1), updated_at = now()
        WHERE lenser_id = NEW.source_profile_id;

      -- Check if was mutual
      SELECT EXISTS (
        SELECT 1 FROM lensers.relationships
        WHERE source_profile_id = NEW.target_profile_id
          AND target_profile_id = NEW.source_profile_id
          AND status = 'accepted'
      ) INTO v_is_mutual;

      IF v_is_mutual THEN
        UPDATE analytics.lenser_stats
          SET mutuals_count = GREATEST(0, mutuals_count - 1), updated_at = now()
          WHERE lenser_id IN (NEW.source_profile_id, NEW.target_profile_id);
      END IF;
    END IF;

  -- Handle DELETE
  ELSIF TG_OP = 'DELETE' AND OLD.status = 'accepted' THEN
    UPDATE analytics.lenser_stats
      SET follower_count = GREATEST(0, follower_count - 1), updated_at = now()
      WHERE lenser_id = OLD.target_profile_id;
    UPDATE analytics.lenser_stats
      SET following_count = GREATEST(0, following_count - 1), updated_at = now()
      WHERE lenser_id = OLD.source_profile_id;
  END IF;

  RETURN NULL;
END;
$$;


ALTER FUNCTION "lensers"."fn_sync_relationship_counts"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."fn_trg_notify_badge_awarded"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
DECLARE
  v_handle TEXT;
BEGIN
  SELECT handle INTO v_handle FROM lensers.profiles WHERE id = NEW.lenser_id;

  PERFORM public.fn_insert_notification(
    NEW.lenser_id,
    'badge_awarded',
    'You earned a badge: ' || NEW.label,
    NEW.description,
    '/lenser/' || COALESCE(v_handle, '') || '/badges',
    jsonb_build_object(
      'badge_id',       NEW.id,
      'badge_name',     NEW.label,
      'badge_type',     NEW.type::text,
      'badge_category', NEW.category::text,
      'icon',           NEW.icon
    )
    -- p_actor_id = NULL
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_badge_awarded failed for lenser %: %', NEW.lenser_id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."fn_trg_notify_badge_awarded"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."fn_xp_on_follow"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'xp', 'public'
    AS $$
DECLARE
  v_app_id uuid;
BEGIN
  -- Only award XP when the relationship status becomes 'accepted'
  IF NEW.status != 'accepted' THEN
    RETURN NEW;
  END IF;
  -- Skip if this is an UPDATE and the status was already 'accepted'
  IF TG_OP = 'UPDATE' AND OLD.status = 'accepted' THEN
    RETURN NEW;
  END IF;

  -- Resolve platform app_id; fall back to forum app
  SELECT id INTO v_app_id FROM xp.apps WHERE slug = 'platform' LIMIT 1;
  IF v_app_id IS NULL THEN
    SELECT id INTO v_app_id FROM xp.apps WHERE slug = 'forum' LIMIT 1;
  END IF;
  IF v_app_id IS NULL THEN
    RETURN NEW;
  END IF;

  -- Award XP to the follower (source). xp.apply enforces daily cap idempotently.
  BEGIN
    PERFORM xp.apply(
      NEW.source_profile_id,
      'INVITE_ACCEPTED',
      'social'::xp.source_enum,
      'relationship',
      NEW.id,
      v_app_id
    );
  EXCEPTION WHEN OTHERS THEN
    NULL; -- non-fatal; never block the follow
  END;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."fn_xp_on_follow"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."get_auth_human_lenser_id"() RETURNS "uuid"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'auth'
    AS $$
  SELECT p.id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.type = 'human'
    AND p.status = 'active'
  ORDER BY p.created_at ASC
  LIMIT 1;
$$;


ALTER FUNCTION "lensers"."get_auth_human_lenser_id"() OWNER TO "postgres";


COMMENT ON FUNCTION "lensers"."get_auth_human_lenser_id"() IS 'Returns the authenticated user''s primary active human lenser profile id. Used by owner-only AI management policies so they keep working after workspace switching activates an AI lenser.';



CREATE OR REPLACE FUNCTION "lensers"."guard_profile_identity_columns"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO ''
    AS $$
BEGIN
  -- SECURITY DEFINER functions and service_role may update any column
  IF current_user IN ('postgres', 'supabase_admin', 'service_role') THEN
    RETURN NEW;
  END IF;

  IF NEW.id             IS DISTINCT FROM OLD.id
    OR NEW.user_id      IS DISTINCT FROM OLD.user_id
    OR NEW.type         IS DISTINCT FROM OLD.type
    OR NEW.status       IS DISTINCT FROM OLD.status
    OR NEW.join_order   IS DISTINCT FROM OLD.join_order
    OR NEW.login_count  IS DISTINCT FROM OLD.login_count
    OR NEW.created_at   IS DISTINCT FROM OLD.created_at
  THEN
    RAISE EXCEPTION 'Cannot modify protected profile columns (id, user_id, type, status, join_order, login_count, created_at)'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."guard_profile_identity_columns"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."init_lenser_engagement_row"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$BEGIN
  INSERT INTO analytics.lenser_stats (lenser_id)
  VALUES (NEW.id)
  ON CONFLICT (lenser_id) DO NOTHING;

  RETURN NEW;
END;$$;


ALTER FUNCTION "lensers"."init_lenser_engagement_row"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."is_active_lenser"("p_user_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$SELECT EXISTS (
    SELECT 1
    FROM lensers.profiles
    WHERE user_id = p_user_id
      AND status = 'active'
      AND deletion_requested_at IS NULL
  );$$;


ALTER FUNCTION "lensers"."is_active_lenser"("p_user_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."log_account_lifecycle_event"("p_profile_id" "uuid", "p_user_id" "uuid", "p_event_type" "text", "p_from_status" "lensers"."lenser_status", "p_to_status" "lensers"."lenser_status", "p_actor_source" "text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'audit', 'auth'
    AS $$
BEGIN
  INSERT INTO audit.account_lifecycle (
    profile_id,
    user_id,
    event_type,
    from_status,
    to_status,
    actor_source,
    metadata
  )
  VALUES (
    p_profile_id,
    p_user_id,
    p_event_type,
    p_from_status,
    p_to_status,
    p_actor_source,
    COALESCE(p_metadata, '{}'::jsonb)
  );
END;
$$;


ALTER FUNCTION "lensers"."log_account_lifecycle_event"("p_profile_id" "uuid", "p_user_id" "uuid", "p_event_type" "text", "p_from_status" "lensers"."lenser_status", "p_to_status" "lensers"."lenser_status", "p_actor_source" "text", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."log_lenser_join"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$begin
  -- Aynı lenser için tekrar yazmaya çalışılırsa dokunma
  insert into analytics.lenser_join_log (lenser_id)
  values (new.id)
  on conflict (lenser_id) do nothing;

  return new;
end;$$;


ALTER FUNCTION "lensers"."log_lenser_join"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."prevent_lenser_join_log_delete"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
BEGIN
  -- Fix: Update error message to point to correct schema (analytics)
  RAISE EXCEPTION 'Deletion from analytics.lenser_join_log is not allowed';
END;
$$;


ALTER FUNCTION "lensers"."prevent_lenser_join_log_delete"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."prevent_lenser_join_log_update"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
BEGIN
  IF old.join_order <> new.join_order OR old.lenser_id <> new.lenser_id THEN
    -- Fix: Update error message to point to correct schema (analytics)
    RAISE EXCEPTION 'join_order and lenser_id are immutable on analytics.lenser_join_log';
  END IF;
  RETURN new;
END;
$$;


ALTER FUNCTION "lensers"."prevent_lenser_join_log_update"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."protect_sensitive_lenser_fields"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
DECLARE
  jwt_role text := current_setting('request.jwt.claim.role', true);
BEGIN
  -- Allow SECURITY DEFINER functions (run as postgres) and direct service_role calls
  IF current_user = 'postgres' OR jwt_role = 'service_role' THEN
    RETURN NEW;
  END IF;

  IF NEW.handle IS DISTINCT FROM OLD.handle THEN
    RAISE EXCEPTION 'handle can only be modified by service_role';
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."protect_sensitive_lenser_fields"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."sync_join_order"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lensers', 'public'
    AS $$BEGIN
  UPDATE lensers.profiles
  SET join_order = NEW.join_order
  WHERE id = NEW.lenser_id;
  RETURN NEW;
END;$$;


ALTER FUNCTION "lensers"."sync_join_order"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."sync_profile_from_auth_metadata"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
BEGIN
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."sync_profile_from_auth_metadata"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_audit_group_membership"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
    PERFORM audit.log_event_v2(
        TG_OP::text,                                           -- event_type
        'system',                                              -- actor_type
        NULL,                                                  -- actor_id
        'lensers',                                             -- entity_schema
        'group_members',                                       -- entity_table
        COALESCE(NEW.group_id, OLD.group_id),                  -- entity_id
        jsonb_build_object(
            'lenser_id', COALESCE(NEW.lenser_id, OLD.lenser_id),
            'role',      COALESCE(NEW.role::text, OLD.role::text),
            'operation', TG_OP
        ),                                                     -- payload
        'info'                                                 -- severity
    );
    RETURN COALESCE(NEW, OLD);
END;
$$;


ALTER FUNCTION "lensers"."trg_audit_group_membership"() OWNER TO "postgres";


COMMENT ON FUNCTION "lensers"."trg_audit_group_membership"() IS 'Audit trigger function for group membership changes. Logs INSERT, UPDATE, and DELETE operations to audit.events via audit.log_event_v2(). SECURITY DEFINER with locked search_path.';



CREATE OR REPLACE FUNCTION "lensers"."trg_award_founder_badges"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
BEGIN
  -- Fix: reference correctly to lensers. schema
  PERFORM lensers.auto_award_badges_from_join_order(NEW.lenser_id);
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."trg_award_founder_badges"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_create_join_log"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$BEGIN
  INSERT INTO analytics.lenser_join_log (lenser_id)
  VALUES (NEW.id);
  RETURN NEW;
END;$$;


ALTER FUNCTION "lensers"."trg_create_join_log"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_handle_deletion_request"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
BEGIN
  -- First deletion request: force the timestamp and immediately deactivate the profile.
  IF NEW.deletion_requested_at IS NOT NULL AND OLD.deletion_requested_at IS NULL THEN
    NEW.status := 'deactivated';
    NEW.deletion_requested_at := now();
    RETURN NEW;
  END IF;

  -- Restoration path: allow lifecycle RPCs to clear the deletion markers.
  IF OLD.deletion_requested_at IS NOT NULL
     AND NEW.deletion_requested_at IS NULL THEN
    RETURN NEW;
  END IF;

  -- Any other mutation of an existing deletion request timestamp remains invalid.
  IF OLD.deletion_requested_at IS NOT NULL
     AND NEW.deletion_requested_at IS DISTINCT FROM OLD.deletion_requested_at THEN
    RAISE EXCEPTION 'Deletion request can only be set once.';
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."trg_handle_deletion_request"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_log_lenser_activity"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$DECLARE
  v_lenser_id uuid;
BEGIN
  -- Find matching Lenser profile
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = NEW.id;

  -- If no lenser exists, ignore
  IF v_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  -- Only act on REAL login event
  IF NEW.last_sign_in_at IS DISTINCT FROM OLD.last_sign_in_at THEN
    INSERT INTO analytics.lenser_activity (lenser_id, activity_type)
    VALUES (v_lenser_id, 'login');
  END IF;

  RETURN NEW;
END;$$;


ALTER FUNCTION "lensers"."trg_log_lenser_activity"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_log_login_from_auth"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'analytics', 'public', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  -- auth.users PK is `id`, not `user_id`
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = NEW.id;

  IF v_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  IF NEW.last_sign_in_at IS DISTINCT FROM OLD.last_sign_in_at THEN
    UPDATE lensers.profiles
    SET
      last_login_at  = NEW.last_sign_in_at,
      last_active_at = now(),
      login_count    = COALESCE(login_count, 0) + 1
    WHERE id = v_lenser_id;

    INSERT INTO analytics.lenser_activity (lenser_id, activity_type)
    VALUES (v_lenser_id, 'login');
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."trg_log_login_from_auth"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_notify_relationship_change"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
DECLARE
  v_actor    lensers.profiles%ROWTYPE;
  v_acceptor lensers.profiles%ROWTYPE;
BEGIN
  SELECT * INTO v_actor FROM lensers.profiles WHERE id = NEW.source_profile_id;

  IF TG_OP = 'INSERT' THEN
    IF NEW.status = 'accepted' THEN
      -- Direct follow (public/community profile) → notify the target
      PERFORM public.fn_insert_notification(
        NEW.target_profile_id,
        'follow_new',
        COALESCE(v_actor.display_name, v_actor.handle) || ' started following you',
        NULL,
        '/lenser/' || v_actor.handle,
        jsonb_build_object(
          'follower_id',           NEW.source_profile_id,
          'follower_handle',       v_actor.handle,
          'follower_display_name', v_actor.display_name,
          'follower_avatar_url',   v_actor.avatar_url
        )
      );
    ELSIF NEW.status = 'pending' THEN
      -- Follow request (private profile)
      PERFORM public.fn_insert_notification(
        NEW.target_profile_id,
        'follow_request',
        COALESCE(v_actor.display_name, v_actor.handle) || ' requested to follow you',
        NULL,
        '/notifications',
        jsonb_build_object(
          'requester_id',           NEW.source_profile_id,
          'requester_handle',       v_actor.handle,
          'requester_display_name', v_actor.display_name,
          'requester_avatar_url',   v_actor.avatar_url
        )
      );
    END IF;

  ELSIF TG_OP = 'UPDATE' AND OLD.status = 'pending' AND NEW.status = 'accepted' THEN
    SELECT * INTO v_acceptor FROM lensers.profiles WHERE id = NEW.target_profile_id;
    PERFORM public.fn_insert_notification(
      NEW.source_profile_id,
      'follow_accepted',
      COALESCE(v_acceptor.display_name, v_acceptor.handle) || ' accepted your follow request',
      NULL,
      '/lenser/' || v_acceptor.handle,
      jsonb_build_object(
        'acceptor_id',           NEW.target_profile_id,
        'acceptor_handle',       v_acceptor.handle,
        'acceptor_display_name', v_acceptor.display_name,
        'acceptor_avatar_url',   v_acceptor.avatar_url
      )
    );
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_relationship_change failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lensers"."trg_notify_relationship_change"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."trg_update_lenser_last_login"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$BEGIN
  -- Only act when real login occurs
  IF NEW.last_sign_in_at IS DISTINCT FROM OLD.last_sign_in_at THEN
    UPDATE lensers.profiles
    SET last_login_at = NEW.last_sign_in_at
    WHERE user_id = NEW.id;
  END IF;

  RETURN NEW;
END;$$;


ALTER FUNCTION "lensers"."trg_update_lenser_last_login"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lensers"."user_owns_lenser"("lenser_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$select exists (
    select 1 from lensers.profiles l
    where l.id = lenser_id
      and l.user_id = auth.uid()
  );$$;


ALTER FUNCTION "lensers"."user_owns_lenser"("lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."ensure_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."ensure_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_build_schedule_condition_context"("p_schedule_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
DECLARE
  v_workflow_id uuid;
  v_prior       jsonb;
  v_succeeded   integer;
  v_failed      integer;
  v_total       integer;
BEGIN
  SELECT s.workflow_id INTO v_workflow_id
  FROM lenses.workflow_schedules s
  WHERE s.id = p_schedule_id;

  IF v_workflow_id IS NULL THEN
    RETURN jsonb_build_object(
      'prior_run_result', NULL,
      'last_24h_stats', jsonb_build_object(
        'succeeded', 0, 'failed', 0, 'total', 0
      ),
      'signal_rpc_result', NULL
    );
  END IF;

  -- Most recent run's result block (NULL-safe).
  SELECT r.metadata -> 'result'
    INTO v_prior
  FROM lenses.workflow_runs r
  WHERE r.workflow_id = v_workflow_id
  ORDER BY r.created_at DESC
  LIMIT 1;

  -- 24h aggregate windowed by created_at.
  SELECT
    COALESCE(SUM(CASE WHEN r.status = 'completed' THEN 1 ELSE 0 END), 0),
    COALESCE(SUM(CASE WHEN r.status = 'failed'    THEN 1 ELSE 0 END), 0),
    COUNT(*)
  INTO v_succeeded, v_failed, v_total
  FROM lenses.workflow_runs r
  WHERE r.workflow_id = v_workflow_id
    AND r.created_at > now() - interval '24 hours';

  RETURN jsonb_build_object(
    'prior_run_result', v_prior,
    'last_24h_stats', jsonb_build_object(
      'succeeded', v_succeeded,
      'failed',    v_failed,
      'total',     v_total
    ),
    -- Future hook: operators may extend this builder to invoke a signal RPC
    -- (e.g. external KPI fetch) and merge the result here. Today: NULL.
    'signal_rpc_result', NULL
  );
END;
$$;


ALTER FUNCTION "lenses"."fn_build_schedule_condition_context"("p_schedule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_build_schedule_condition_context"("p_schedule_id" "uuid") IS 'Phase W2: builds the jsonb context the dispatcher feeds to automation.fn_eval_filter when a schedule has pre_dispatch_condition. Shape: { prior_run_result, last_24h_stats:{succeeded,failed,total}, signal_rpc_result }. signal_rpc_result is reserved for a future operator hook and is currently always NULL.';



CREATE OR REPLACE FUNCTION "lenses"."fn_check_calendar"("p_calendar_id" "uuid", "p_now" timestamp with time zone DEFAULT "now"()) RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
DECLARE
  v_kind     text;
  v_dates    date[];
  v_tz       text;
  v_local    date;
  v_in_set   boolean;
BEGIN
  IF p_calendar_id IS NULL THEN
    RETURN true;
  END IF;

  SELECT kind, dates, timezone
    INTO v_kind, v_dates, v_tz
  FROM lenses.schedule_calendars
  WHERE id = p_calendar_id;

  IF NOT FOUND THEN
    RETURN true;
  END IF;

  -- Resolve the wall-clock date in the calendar's timezone.
  BEGIN
    v_local := (p_now AT TIME ZONE v_tz)::date;
  EXCEPTION WHEN OTHERS THEN
    -- Bad timezone string: fail closed-ish — allow dispatch but log nothing.
    RETURN true;
  END;

  v_in_set := v_local = ANY(COALESCE(v_dates, ARRAY[]::date[]));

  IF v_kind = 'skip_dates' THEN
    RETURN NOT v_in_set;
  ELSIF v_kind = 'only_dates' THEN
    RETURN v_in_set;
  END IF;

  -- Defensive: unknown kind => allow.
  RETURN true;
END;
$$;


ALTER FUNCTION "lenses"."fn_check_calendar"("p_calendar_id" "uuid", "p_now" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_check_calendar"("p_calendar_id" "uuid", "p_now" timestamp with time zone) IS 'Phase W1: returns TRUE when dispatch is allowed for the calendar overlay at p_now. NULL calendar or missing row => TRUE. Resolves p_now into the calendar timezone before checking date membership.';



CREATE OR REPLACE FUNCTION "lenses"."fn_check_dag_acyclic"("p_workflow_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$
DECLARE
  v_node_id      uuid;
  v_stack        uuid[];
  v_visited      uuid[];
  v_rec_stack    uuid[];
  v_has_cycle    boolean := false;
  v_adj          record;
  v_current      uuid;
  v_dfs_stack    uuid[];
  v_dfs_parent   uuid[];
BEGIN
  -- Collect all unique node IDs involved in edges for this workflow
  -- Use iterative DFS to avoid stack overflow on deep graphs

  FOR v_node_id IN
    SELECT DISTINCT unnest(ARRAY[source_node_id, target_node_id])
    FROM lenses.workflow_edges
    WHERE workflow_id = p_workflow_id
  LOOP
    -- Skip already fully visited nodes
    IF v_node_id = ANY(v_visited) THEN
      CONTINUE;
    END IF;

    -- Iterative DFS using explicit stack
    v_dfs_stack := ARRAY[v_node_id];
    v_rec_stack := ARRAY[]::uuid[];

    WHILE array_length(v_dfs_stack, 1) > 0 LOOP
      v_current := v_dfs_stack[array_length(v_dfs_stack, 1)];
      v_dfs_stack := v_dfs_stack[1:array_length(v_dfs_stack, 1) - 1];

      -- If current is a sentinel (NULL-marked pop), remove from rec_stack
      IF v_current IS NULL THEN
        IF array_length(v_dfs_stack, 1) > 0 THEN
          v_current := v_dfs_stack[array_length(v_dfs_stack, 1)];
          v_dfs_stack := v_dfs_stack[1:array_length(v_dfs_stack, 1) - 1];
          v_rec_stack := array_remove(v_rec_stack, v_current);
        END IF;
        CONTINUE;
      END IF;

      IF v_current = ANY(v_rec_stack) THEN
        -- Back edge found → cycle
        RETURN false;
      END IF;

      IF v_current = ANY(v_visited) THEN
        CONTINUE;
      END IF;

      v_visited := array_append(v_visited, v_current);
      v_rec_stack := array_append(v_rec_stack, v_current);

      -- Push sentinel to pop from rec_stack when backtracking
      v_dfs_stack := array_append(v_dfs_stack, v_current);
      v_dfs_stack := array_append(v_dfs_stack, NULL::uuid);

      -- Push all neighbors
      FOR v_adj IN
        SELECT target_node_id
        FROM lenses.workflow_edges
        WHERE workflow_id = p_workflow_id
          AND source_node_id = v_current
      LOOP
        IF v_adj.target_node_id = ANY(v_rec_stack) THEN
          RETURN false;  -- Cycle detected
        END IF;
        IF NOT (v_adj.target_node_id = ANY(v_visited)) THEN
          v_dfs_stack := array_append(v_dfs_stack, v_adj.target_node_id);
        END IF;
      END LOOP;
    END LOOP;
  END LOOP;

  RETURN true;  -- No cycles found
END;
$$;


ALTER FUNCTION "lenses"."fn_check_dag_acyclic"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_check_dag_acyclic_kahn"("p_workflow_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$
DECLARE
  v_total_nodes  integer;
  v_processed    integer := 0;
  v_queue        uuid[];
  v_current      uuid;
  v_qi           integer := 1;
  v_in_degree    record;
BEGIN
  -- Count distinct nodes
  SELECT count(DISTINCT node_id) INTO v_total_nodes
  FROM (
    SELECT source_node_id AS node_id FROM lenses.workflow_edges WHERE workflow_id = p_workflow_id
    UNION
    SELECT target_node_id AS node_id FROM lenses.workflow_edges WHERE workflow_id = p_workflow_id
  ) nodes;

  IF v_total_nodes = 0 THEN
    RETURN true;  -- Empty graph is acyclic
  END IF;

  -- Build in-degree and find roots (in_degree = 0)
  -- Nodes that only appear as source (never as target) have in_degree = 0
  WITH all_nodes AS (
    SELECT DISTINCT node_id FROM (
      SELECT source_node_id AS node_id FROM lenses.workflow_edges WHERE workflow_id = p_workflow_id
      UNION
      SELECT target_node_id AS node_id FROM lenses.workflow_edges WHERE workflow_id = p_workflow_id
    ) n
  ),
  in_degrees AS (
    SELECT an.node_id, coalesce(cnt.c, 0) AS deg
    FROM all_nodes an
    LEFT JOIN (
      SELECT target_node_id, count(*) AS c
      FROM lenses.workflow_edges
      WHERE workflow_id = p_workflow_id
      GROUP BY target_node_id
    ) cnt ON cnt.target_node_id = an.node_id
  )
  SELECT array_agg(node_id) INTO v_queue
  FROM in_degrees
  WHERE deg = 0;

  IF v_queue IS NULL THEN
    RETURN false;  -- All nodes have incoming edges → cycle
  END IF;

  -- Process queue (Kahn's)
  WHILE v_qi <= array_length(v_queue, 1) LOOP
    v_current := v_queue[v_qi];
    v_qi := v_qi + 1;
    v_processed := v_processed + 1;

    -- For each neighbor of current, decrement in-degree conceptually
    -- by checking if all predecessors have been processed
    FOR v_in_degree IN
      SELECT DISTINCT e.target_node_id
      FROM lenses.workflow_edges e
      WHERE e.workflow_id = p_workflow_id
        AND e.source_node_id = v_current
    LOOP
      -- Check if all incoming edges to this target come from already-processed nodes
      IF NOT EXISTS (
        SELECT 1
        FROM lenses.workflow_edges e2
        WHERE e2.workflow_id = p_workflow_id
          AND e2.target_node_id = v_in_degree.target_node_id
          AND e2.source_node_id <> ALL(v_queue[1:v_qi-1])
      ) THEN
        v_queue := array_append(v_queue, v_in_degree.target_node_id);
      END IF;
    END LOOP;
  END LOOP;

  RETURN v_processed = v_total_nodes;
END;
$$;


ALTER FUNCTION "lenses"."fn_check_dag_acyclic_kahn"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_check_workflow_budget"("p_run_id" "uuid", "p_estimated_cost" integer) RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$
DECLARE
  v_budget   integer;
  v_spent    integer;
BEGIN
  -- Lock the run row to prevent concurrent budget checks racing
  PERFORM pg_advisory_xact_lock(hashtext(p_run_id::text));

  SELECT budget_credits, spent_credits
  INTO v_budget, v_spent
  FROM lenses.workflow_runs
  WHERE id = p_run_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN false;
  END IF;

  -- NULL budget = unlimited
  IF v_budget IS NULL THEN
    RETURN true;
  END IF;

  RETURN (v_spent + p_estimated_cost) <= v_budget;
END;
$$;


ALTER FUNCTION "lenses"."fn_check_workflow_budget"("p_run_id" "uuid", "p_estimated_cost" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_claim_scheduled_workflow_run"("p_worker_id" "text") RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "triggered_by" "uuid", "context_inputs" "jsonb", "global_model_id" "text", "ai_lenser_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_run lenses.workflow_runs;
BEGIN
  -- GUARD: system kill switch blocks all scheduled-run claims immediately.
  IF public.fn_kill_switch_active('system') THEN
    RETURN;  -- empty result set — worker backs off silently
  END IF;

  SELECT *
  INTO   v_run
  FROM   lenses.workflow_runs
  WHERE  status = 'pending'
    AND  trigger_mode = 'schedule'
  ORDER BY created_at ASC
  LIMIT  1
  FOR UPDATE SKIP LOCKED;

  IF NOT FOUND THEN
    RETURN;
  END IF;

  UPDATE lenses.workflow_runs
  SET    status     = 'running',
         started_at = COALESCE(started_at, now())
  WHERE  id = v_run.id;

  RETURN QUERY
  SELECT
    v_run.id,
    v_run.workflow_id,
    v_run.schedule_id,
    v_run.triggered_by,
    v_run.context_inputs,
    v_run.global_model_id,
    v_run.ai_lenser_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_claim_scheduled_workflow_run"("p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_claim_scheduled_workflow_run"("p_worker_id" "text") IS 'Phase 24-A + Emergency Stop: atomically claims the next pending scheduled workflow run. Returns empty result set when the system kill switch is active, causing workers to back off without processing.';



CREATE OR REPLACE FUNCTION "lenses"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id       uuid;
  v_src_vis         content.visibility_enum;
  v_src_status      content.content_status;
  v_template        text;
  v_title           text;
  v_description     text;
  v_language        text;
  v_resolved_ver_id uuid;
  v_new_lens_id     uuid;
  v_new_version     lenses.versions;
BEGIN
  -- Auth check
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Fork cooldown: max one fork per 5 minutes per user
  IF EXISTS (
    SELECT 1 FROM lenses.lenses
    WHERE lenser_id = v_caller_id
      AND parent_lens_id IS NOT NULL
      AND created_at > NOW() - INTERVAL '5 minutes'
  ) THEN
    RAISE EXCEPTION 'rate_limit_exceeded'
      USING DETAIL = 'You can only fork a lens once every 5 minutes.',
            ERRCODE = 'P0429';
  END IF;

  -- Source lens must be public + published
  SELECT l.visibility, l.status
    INTO v_src_vis, v_src_status
    FROM lenses.lenses l
   WHERE l.id = p_source_lens_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Source lens % not found', p_source_lens_id
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_src_vis <> 'public'::"content"."visibility_enum"
     OR v_src_status <> 'published'::"content"."content_status" THEN
    RAISE EXCEPTION 'Cannot clone: source lens must be public and published'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Resolve template_body + version id from specific published version or latest published
  IF p_version_id IS NOT NULL THEN
    SELECT v.id, v.template_body
      INTO v_resolved_ver_id, v_template
      FROM lenses.versions v
     WHERE v.id      = p_version_id
       AND v.lens_id = p_source_lens_id
       AND v.status  = 'published'::"content"."content_status";

    IF NOT FOUND THEN
      RAISE EXCEPTION 'Published version % not found for lens %',
        p_version_id, p_source_lens_id
        USING ERRCODE = 'no_data_found';
    END IF;
  ELSE
    SELECT v.id, v.template_body
      INTO v_resolved_ver_id, v_template
      FROM lenses.versions v
     WHERE v.lens_id = p_source_lens_id
       AND v.status  = 'published'::"content"."content_status"
     ORDER BY v.version_number DESC
     LIMIT 1;

    IF NOT FOUND THEN
      RAISE EXCEPTION 'No published version found for lens %', p_source_lens_id
        USING ERRCODE = 'no_data_found';
    END IF;
  END IF;

  -- Resolve original translation
  SELECT t.title, t.description, t.language_code
    INTO v_title, v_description, v_language
    FROM content.entity_translations t
   WHERE t.entity_id   = p_source_lens_id
     AND t.entity_type = 'lens'::"content"."entity_type_enum"
     AND t.is_original = true
   LIMIT 1;

  -- Create the new (cloned) lens record — record which version it was forked from
  INSERT INTO lenses.lenses (
    lenser_id, visibility, status,
    parent_lens_id, forked_from_version_id
  ) VALUES (
    v_caller_id,
    'private'::"content"."visibility_enum",
    'published'::"content"."content_status",
    p_source_lens_id,
    v_resolved_ver_id
  )
  RETURNING id INTO v_new_lens_id;

  -- Create initial draft version via upsert (brand-new lens has no prior versions)
  v_new_version := lenses.fn_upsert_draft_version(
    v_new_lens_id,
    v_template,
    'Cloned from ' || coalesce(v_title, 'Untitled')
  );

  -- Set HEAD
  UPDATE lenses.lenses
     SET head_version_id = v_new_version.id
   WHERE id = v_new_lens_id;

  -- Clone version_parameters (label + tool_id) from source version → new version
  INSERT INTO lenses.version_parameters (version_id, label, tool_id)
  SELECT v_new_version.id, vp.label, vp.tool_id
  FROM lenses.version_parameters vp
  WHERE vp.version_id = v_resolved_ver_id
  LIMIT 200;

  -- Clone steps (version-specific + global)
  INSERT INTO lenses.steps (
    lens_id, version_id,
    ordinal, step_type, instruction,
    model_id, input_map, output_key, sub_lens_id
  )
  SELECT
    v_new_lens_id,
    v_new_version.id,
    s.ordinal, s.step_type, s.instruction,
    s.model_id, s.input_map, s.output_key, s.sub_lens_id
  FROM lenses.steps s
  WHERE s.lens_id = p_source_lens_id
    AND (
      p_version_id IS NULL
      OR s.version_id = p_version_id
      OR s.version_id IS NULL
    )
  LIMIT 500;

  -- Copy translation with "Fork of" prefix
  INSERT INTO content.entity_translations (
    entity_type, entity_id, language_code, is_original,
    title, description, content
  ) VALUES (
    'lens'::"content"."entity_type_enum",
    v_new_lens_id,
    coalesce(v_language, 'en'),
    true,
    'Fork of ' || coalesce(v_title, 'Untitled'),
    v_description,
    v_template
  );

  -- Copy tag associations
  INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
  SELECT
    'lens'::"content"."entity_type_enum",
    v_new_lens_id,
    tm.tag_id
  FROM content.tag_map tm
  WHERE tm.entity_type = 'lens'::"content"."entity_type_enum"
    AND tm.entity_id   = p_source_lens_id
  LIMIT 50;

  RETURN v_new_lens_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") IS 'Forks a public published lens into a private draft clone owned by the caller.
   Sets head_version_id and forked_from_version_id (the specific version cloned).
   Clones: version_parameters (label + tool_id), steps (version-specific + global),
   translation (''Fork of'' prefix), and tag associations. SECURITY DEFINER: ownership
   and visibility checked internally.';



CREATE OR REPLACE FUNCTION "lenses"."fn_clone_workflow"("p_source_workflow_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id       uuid;
  v_src             record;
  v_new_workflow_id uuid;
  v_node_map        jsonb := '{}';
  v_old_node        record;
  v_new_node_id     uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Fork cooldown: max one fork per 5 minutes per user
  IF EXISTS (
    SELECT 1 FROM lenses.workflows
    WHERE lenser_id = v_caller_id
      AND parent_workflow_id IS NOT NULL
      AND created_at > NOW() - INTERVAL '5 minutes'
  ) THEN
    RAISE EXCEPTION 'rate_limit_exceeded'
      USING DETAIL = 'You can only fork a workflow once every 5 minutes.',
            ERRCODE = 'P0429';
  END IF;

  -- Source must be public
  SELECT id, title, description, visibility
  INTO v_src
  FROM lenses.workflows
  WHERE id = p_source_workflow_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Workflow not found'
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_src.visibility <> 'public' THEN
    RAISE EXCEPTION 'Cannot fork: workflow must be public'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Create forked workflow
  INSERT INTO lenses.workflows (lenser_id, title, description, visibility, parent_workflow_id)
  VALUES (v_caller_id, 'Fork of ' || v_src.title, v_src.description, 'private', p_source_workflow_id)
  RETURNING id INTO v_new_workflow_id;

  -- Update fork count on source
  UPDATE lenses.workflows
  SET fork_count = fork_count + 1
  WHERE id = p_source_workflow_id;

  -- Copy nodes (remap IDs)
  FOR v_old_node IN
    SELECT id, lens_id, version_id, position_x, position_y, label, ordinal, config
    FROM lenses.workflow_nodes
    WHERE workflow_id = p_source_workflow_id
    ORDER BY ordinal
  LOOP
    v_new_node_id := gen_random_uuid();
    INSERT INTO lenses.workflow_nodes (id, workflow_id, lens_id, version_id, position_x, position_y, label, ordinal, config)
    VALUES (v_new_node_id, v_new_workflow_id, v_old_node.lens_id, v_old_node.version_id,
            v_old_node.position_x, v_old_node.position_y, v_old_node.label, v_old_node.ordinal, v_old_node.config);
    v_node_map := v_node_map || jsonb_build_object(v_old_node.id::text, v_new_node_id::text);
  END LOOP;

  -- Copy edges (remap node IDs)
  INSERT INTO lenses.workflow_edges (workflow_id, source_node_id, target_node_id, source_output_key, target_param_label)
  SELECT
    v_new_workflow_id,
    (v_node_map ->> e.source_node_id::text)::uuid,
    (v_node_map ->> e.target_node_id::text)::uuid,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  WHERE e.workflow_id = p_source_workflow_id;

  RETURN v_new_workflow_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_clone_workflow"("p_source_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_count_recent_runs"("p_lenser_id" "uuid", "p_window_seconds" integer) RETURNS integer
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses'
    AS $$
  SELECT COUNT(*)::int
  FROM lenses.workflow_runs r
  WHERE r.triggered_by = p_lenser_id
    AND r.created_at > now() - make_interval(secs => GREATEST(p_window_seconds, 0)::numeric);
$$;


ALTER FUNCTION "lenses"."fn_count_recent_runs"("p_lenser_id" "uuid", "p_window_seconds" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_count_recent_runs"("p_lenser_id" "uuid", "p_window_seconds" integer) IS 'Phase 9 — returns the number of workflow_runs started by a lenser in the last N seconds (used by rate limiter).';



CREATE TABLE IF NOT EXISTS "lenses"."versions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lens_id" "uuid" NOT NULL,
    "version_number" integer NOT NULL,
    "template_body" "text" NOT NULL,
    "status" "content"."content_status" DEFAULT 'draft'::"content"."content_status" NOT NULL,
    "changelog" "text",
    "parent_version_id" "uuid",
    "published_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "input_contract" "jsonb",
    "output_contract" "jsonb",
    "content_hash" "bytea",
    "semver" "text",
    CONSTRAINT "lens_versions_published_at_check" CHECK ((("status" <> 'published'::"content"."content_status") OR ("published_at" IS NOT NULL))),
    CONSTRAINT "lens_versions_template_body_min_length" CHECK (("length"(TRIM(BOTH FROM "template_body")) >= 50)),
    CONSTRAINT "template_body_not_blank" CHECK (("length"(TRIM(BOTH FROM "template_body")) > 0)),
    CONSTRAINT "versions_content_hash_length_check" CHECK ((("content_hash" IS NULL) OR ("octet_length"("content_hash") = 32))),
    CONSTRAINT "versions_input_contract_shape_check" CHECK ((("input_contract" IS NULL) OR (("jsonb_typeof"("input_contract") = 'object'::"text") AND ("jsonb_typeof"(("input_contract" -> 'kind'::"text")) = 'string'::"text")))),
    CONSTRAINT "versions_output_contract_shape_check" CHECK ((("output_contract" IS NULL) OR (("jsonb_typeof"("output_contract") = 'object'::"text") AND ("jsonb_typeof"(("output_contract" -> 'kind'::"text")) = 'string'::"text") AND ("jsonb_typeof"(("output_contract" -> 'artifactKind'::"text")) = 'string'::"text")))),
    CONSTRAINT "versions_semver_format_check" CHECK ((("semver" IS NULL) OR ("semver" ~ '^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9]+(\.[0-9]+)?)?$'::"text")))
);

ALTER TABLE ONLY "lenses"."versions" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."versions" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."versions" IS 'Immutable version snapshots of prompt assets. Draft versions are editable; published versions cannot be modified. Parameters belong to versions, not to the parent prompt asset.';



COMMENT ON COLUMN "lenses"."versions"."parent_version_id" IS 'Version-level fork lineage (v3 branched from v2). Distinct from content.lenses.parent_lens_id (asset-level fork).';



COMMENT ON COLUMN "lenses"."versions"."input_contract" IS 'Typed input contract (LensInputContract in libs/types). Optional; when set the workflow engine validates rendered node inputs against it.';



COMMENT ON COLUMN "lenses"."versions"."output_contract" IS 'Typed output contract (LensOutputContract in libs/types). Optional; when set the workflow engine validates the returned envelope against it.';



COMMENT ON COLUMN "lenses"."versions"."content_hash" IS 'sha256 digest (32 bytes) of the canonical Lens Contract body. Populated by the publish RPC; NULL for legacy rows pending backfill.';



COMMENT ON COLUMN "lenses"."versions"."semver" IS 'Semver assigned at publish (e.g. 2.3.1, 2.4.0-rc.1). Bump level enforced by diff against prior stable contract.';



CREATE OR REPLACE FUNCTION "lenses"."fn_create_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text" DEFAULT NULL::"text", "p_parent_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "lenses"."versions"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id  uuid;
  v_next_num   int;
  v_result     lenses.versions;
BEGIN
  -- Auth
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Ownership
  IF NOT EXISTS (
    SELECT 1 FROM lenses.lenses
    WHERE id = p_lens_id AND lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'Permission denied: you do not own lens %', p_lens_id
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Minimum length
  IF length(trim(p_template_body)) < 50 THEN
    RAISE EXCEPTION 'template_body must be at least 50 characters (got %)',
      length(trim(p_template_body))
      USING ERRCODE = 'check_violation';
  END IF;

  -- Next version number (append-only: always one higher than current max)
  SELECT coalesce(max(version_number), 0) + 1
    INTO v_next_num
    FROM lenses.versions
   WHERE lens_id = p_lens_id;

  INSERT INTO lenses.versions (
    lens_id, version_number, template_body,
    status, changelog, parent_version_id
  )
  VALUES (
    p_lens_id,
    v_next_num,
    p_template_body,
    'draft'::"content"."content_status",
    coalesce(p_changelog, 'Draft v' || v_next_num),
    p_parent_version_id
  )
  RETURNING * INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "lenses"."fn_create_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_create_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") IS 'Append-only draft creator: always INSERTs a new lenses.versions row with the next
   sequential version_number. Never updates an existing version. Use for all
   user-initiated edit saves to ensure full edit history is preserved.
   fn_upsert_draft_version is retained for internal creation flows (fn_create_lens,
   fn_clone_lens). SECURITY DEFINER: ownership checked internally.';



CREATE OR REPLACE FUNCTION "lenses"."fn_create_lens"("p_visibility" "content"."visibility_enum", "p_template_body" "text", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_language_code" "text" DEFAULT 'en'::"text", "p_params" "jsonb" DEFAULT '[]'::"jsonb", "p_tag_ids" "uuid"[] DEFAULT '{}'::"uuid"[], "p_parent_lens_id" "uuid" DEFAULT NULL::"uuid", "p_forked_from_execution_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id   uuid;
  v_new_lens_id uuid;
  v_new_version lenses.versions;
  v_tag_id      uuid;
  v_param       jsonb;
  v_param_id    uuid;
  v_text_tool   uuid;
  v_uuid_body   text;  -- template_body with [[:uuid]] tokens (stored in versions)
BEGIN
  -- Auth
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF length(trim(p_template_body)) < 50 THEN
    RAISE EXCEPTION 'template_body must be at least 50 characters (got %)',
      length(trim(p_template_body))
      USING ERRCODE = 'check_violation';
  END IF;

  -- 1. Insert base lens record
  INSERT INTO lenses.lenses (
    lenser_id, visibility, status,
    parent_lens_id, forked_from_execution_id
  ) VALUES (
    v_caller_id,
    p_visibility,
    'published'::content.content_status,
    p_parent_lens_id,
    p_forked_from_execution_id
  )
  RETURNING id INTO v_new_lens_id;

  -- 2. Create initial draft version (validates 50-char minimum)
  v_new_version := lenses.fn_create_draft_version(
    v_new_lens_id,
    p_template_body,
    'Initial version'
  );

  -- 3. Set HEAD to the initial draft version
  UPDATE lenses.lenses
  SET head_version_id = v_new_version.id
  WHERE id = v_new_lens_id;

  -- 4. Insert version_parameters and rewrite template_body to [[:uuid]] format
  v_uuid_body := p_template_body;

  IF p_params IS NOT NULL AND jsonb_array_length(p_params) > 0 THEN
    SELECT id INTO v_text_tool FROM lenses.tools WHERE key = 'text' LIMIT 1;

    FOR v_param IN SELECT * FROM jsonb_array_elements(p_params) LOOP
      INSERT INTO lenses.version_parameters (version_id, label, tool_id)
      VALUES (
        v_new_version.id,
        v_param->>'label',
        COALESCE(
          NULLIF(v_param->>'tool_id', '')::uuid,
          v_text_tool
        )
      )
      RETURNING id INTO v_param_id;

      -- Replace [[label]] → [[:uuid]] in the stored body
      v_uuid_body := replace(
        v_uuid_body,
        '[[' || (v_param->>'label') || ']]',
        '[[:'|| v_param_id::text ||']]'
      );
    END LOOP;

    -- Update version row with rewritten body ([[:uuid]] format)
    UPDATE lenses.versions
    SET template_body = v_uuid_body
    WHERE id = v_new_version.id;
  END IF;

  -- 5. Insert original translation — keeps human-readable [[label]] format
  INSERT INTO content.entity_translations (
    entity_type, entity_id, language_code, is_original,
    title, description, content
  ) VALUES (
    'lens'::content.entity_type_enum,
    v_new_lens_id,
    COALESCE(p_language_code, 'en'),
    true,
    p_title,
    p_description,
    p_template_body  -- human-readable [[label]] format
  );

  -- 6. Insert tag associations
  IF p_tag_ids IS NOT NULL AND array_length(p_tag_ids, 1) > 0 THEN
    FOREACH v_tag_id IN ARRAY p_tag_ids LOOP
      INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
      VALUES ('lens'::content.entity_type_enum, v_new_lens_id, v_tag_id)
      ON CONFLICT DO NOTHING;
    END LOOP;
  END IF;

  RETURN v_new_lens_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_create_lens"("p_visibility" "content"."visibility_enum", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_create_lens"("p_visibility" "content"."visibility_enum", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") IS 'GRASP Creator: creates a lens with its initial draft version, version_parameters, translation, and tag associations atomically. p_params is a jsonb array of parameter definition objects — INSERTed into lenses.version_parameters (not entity_translations). Sets head_version_id. SECURITY DEFINER: auth checked internally.';



CREATE OR REPLACE FUNCTION "lenses"."fn_create_workflow_execution_snapshot"("p_workflow_id" "uuid", "p_actor_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public', 'extensions'
    AS $$
DECLARE
  v_actor        UUID := COALESCE(p_actor_id, lensers.get_auth_lenser_id());
  v_next_version INTEGER;
  v_version_id   UUID;
  v_node_map     JSONB := '{}'::jsonb;
  v_old_node     RECORD;
  v_new_node_id  UUID;
  v_resolved_version_id UUID;
  v_hash         TEXT;
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflows w
    WHERE w.id = p_workflow_id
      AND w.archived_at IS NULL
      AND w.deleted_at IS NULL
      AND (w.visibility = 'public' OR w.lenser_id = v_actor OR public.fn_lifecycle_is_service_role())
  ) THEN
    RAISE EXCEPTION 'workflow_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  SELECT COALESCE(max(version_number), 0) + 1
  INTO v_next_version
  FROM lenses.workflow_versions
  WHERE workflow_id = p_workflow_id;

  INSERT INTO lenses.workflow_versions (workflow_id, version_number, changelog, status, created_by)
  VALUES (p_workflow_id, v_next_version, 'Execution snapshot', 'snapshot', v_actor)
  RETURNING id INTO v_version_id;

  FOR v_old_node IN
    SELECT n.id, n.lens_id, n.version_id, n.position_x, n.position_y, n.label, n.ordinal, n.config
    FROM lenses.workflow_nodes n
    WHERE n.workflow_id = p_workflow_id
    ORDER BY n.ordinal, n.id
  LOOP
    SELECT COALESCE(
      v_old_node.version_id,
      (
        SELECT v.id
        FROM lenses.versions v
        WHERE v.lens_id = v_old_node.lens_id
          AND v.status = 'published'::content.content_status
        ORDER BY v.version_number DESC
        LIMIT 1
      ),
      l.head_version_id
    )
    INTO v_resolved_version_id
    FROM lenses.lenses l
    WHERE l.id = v_old_node.lens_id
      AND l.archived_at IS NULL
      AND l.deleted_at IS NULL;

    IF v_resolved_version_id IS NULL THEN
      RAISE EXCEPTION 'workflow_node_lens_unavailable: %', v_old_node.lens_id USING ERRCODE = '42501';
    END IF;

    v_new_node_id := gen_random_uuid();

    INSERT INTO lenses.workflow_version_nodes (
      id,
      workflow_version_id,
      source_node_id,
      lens_id,
      version_id,
      position_x,
      position_y,
      label,
      ordinal,
      config
    ) VALUES (
      v_new_node_id,
      v_version_id,
      v_old_node.id,
      v_old_node.lens_id,
      v_resolved_version_id,
      v_old_node.position_x,
      v_old_node.position_y,
      v_old_node.label,
      v_old_node.ordinal,
      v_old_node.config
    );

    v_node_map := v_node_map || jsonb_build_object(v_old_node.id::text, v_new_node_id::text);
  END LOOP;

  INSERT INTO lenses.workflow_version_edges (
    workflow_version_id,
    source_node_id,
    target_node_id,
    source_output_key,
    target_param_label
  )
  SELECT
    v_version_id,
    (v_node_map ->> e.source_node_id::text)::uuid,
    (v_node_map ->> e.target_node_id::text)::uuid,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  WHERE e.workflow_id = p_workflow_id
    AND v_node_map ? e.source_node_id::text
    AND v_node_map ? e.target_node_id::text;

  v_hash := lenses.fn_workflow_snapshot_hash(v_version_id);

  UPDATE lenses.workflow_versions
     SET snapshot_hash = v_hash
   WHERE id = v_version_id;

  PERFORM public.fn_log_artifact_lifecycle_event(
    'workflow',
    p_workflow_id,
    'version_snapshot_create',
    v_actor,
    jsonb_build_object('workflow_version_id', v_version_id, 'snapshot_hash', v_hash)
  );

  RETURN v_version_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_create_workflow_execution_snapshot"("p_workflow_id" "uuid", "p_actor_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id     uuid;
  v_owner_id      uuid;
  v_next_version  integer;
  v_version_id    uuid;
  v_node_map      jsonb := '{}';  -- old_node_id → new_node_id
  v_old_node      record;
  v_new_node_id   uuid;
BEGIN
  -- Auth check
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Ownership check
  SELECT lenser_id INTO v_owner_id
  FROM lenses.workflows
  WHERE id = p_workflow_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Workflow not found'
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_owner_id <> v_caller_id THEN
    RAISE EXCEPTION 'Only the workflow owner can create versions'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Compute next version number
  SELECT coalesce(max(version_number), 0) + 1 INTO v_next_version
  FROM lenses.workflow_versions
  WHERE workflow_id = p_workflow_id;

  -- Create version record
  INSERT INTO lenses.workflow_versions (workflow_id, version_number, changelog, status, created_by)
  VALUES (p_workflow_id, v_next_version, p_changelog, 'draft', v_caller_id)
  RETURNING id INTO v_version_id;

  -- Snapshot nodes (need to map old IDs to new IDs for edge references)
  FOR v_old_node IN
    SELECT id, lens_id, version_id, position_x, position_y, label, ordinal, config
    FROM lenses.workflow_nodes
    WHERE workflow_id = p_workflow_id
    ORDER BY ordinal
  LOOP
    v_new_node_id := gen_random_uuid();

    INSERT INTO lenses.workflow_version_nodes (
      id, workflow_version_id, lens_id, version_id,
      position_x, position_y, label, ordinal, config
    ) VALUES (
      v_new_node_id, v_version_id, v_old_node.lens_id, v_old_node.version_id,
      v_old_node.position_x, v_old_node.position_y, v_old_node.label,
      v_old_node.ordinal, v_old_node.config
    );

    v_node_map := v_node_map || jsonb_build_object(v_old_node.id::text, v_new_node_id::text);
  END LOOP;

  -- Snapshot edges (remap node IDs)
  INSERT INTO lenses.workflow_version_edges (
    workflow_version_id, source_node_id, target_node_id,
    source_output_key, target_param_label
  )
  SELECT
    v_version_id,
    (v_node_map ->> e.source_node_id::text)::uuid,
    (v_node_map ->> e.target_node_id::text)::uuid,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  WHERE e.workflow_id = p_workflow_id;

  RETURN v_version_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_cron_field_matches"("p_field" "text", "p_value" integer) RETURNS boolean
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
DECLARE
  v_part text;
  v_base text;
  v_step integer;
  v_start integer;
  v_end integer;
  v_exact integer;
BEGIN
  IF p_field IS NULL OR btrim(p_field) = '' THEN
    RETURN false;
  END IF;

  IF btrim(p_field) = '*' THEN
    RETURN true;
  END IF;

  FOREACH v_part IN ARRAY regexp_split_to_array(replace(btrim(p_field), ' ', ''), ',') LOOP
    CONTINUE WHEN v_part = '';

    IF position('/' IN v_part) > 0 THEN
      v_base := split_part(v_part, '/', 1);
      v_step := split_part(v_part, '/', 2)::integer;
      CONTINUE WHEN v_step IS NULL OR v_step <= 0;

      IF v_base = '*' THEN
        IF mod(p_value, v_step) = 0 THEN
          RETURN true;
        END IF;
      ELSIF position('-' IN v_base) > 0 THEN
        v_start := split_part(v_base, '-', 1)::integer;
        v_end := split_part(v_base, '-', 2)::integer;
        IF p_value BETWEEN v_start AND v_end
           AND mod(p_value - v_start, v_step) = 0 THEN
          RETURN true;
        END IF;
      ELSE
        v_exact := v_base::integer;
        IF p_value >= v_exact
           AND mod(p_value - v_exact, v_step) = 0 THEN
          RETURN true;
        END IF;
      END IF;
    ELSIF position('-' IN v_part) > 0 THEN
      v_start := split_part(v_part, '-', 1)::integer;
      v_end := split_part(v_part, '-', 2)::integer;
      IF p_value BETWEEN v_start AND v_end THEN
        RETURN true;
      END IF;
    ELSE
      v_exact := v_part::integer;
      IF p_value = v_exact THEN
        RETURN true;
      END IF;
    END IF;
  END LOOP;

  RETURN false;
EXCEPTION
  WHEN invalid_text_representation THEN
    RETURN false;
END;
$$;


ALTER FUNCTION "lenses"."fn_cron_field_matches"("p_field" "text", "p_value" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_cron_matches_now"("p_cron_expr" "text", "p_now" timestamp with time zone DEFAULT "now"()) RETURNS boolean
    LANGUAGE "plpgsql" STABLE
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
DECLARE
  v_parts text[];
  v_minute integer := EXTRACT(minute FROM p_now)::integer;
  v_hour integer := EXTRACT(hour FROM p_now)::integer;
  v_dom integer := EXTRACT(day FROM p_now)::integer;
  v_month integer := EXTRACT(month FROM p_now)::integer;
  v_dow0 integer := EXTRACT(dow FROM p_now)::integer;
  v_dow7 integer := CASE WHEN EXTRACT(dow FROM p_now)::integer = 0 THEN 7 ELSE EXTRACT(dow FROM p_now)::integer END;
BEGIN
  v_parts := regexp_split_to_array(trim(COALESCE(p_cron_expr, '')), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RETURN false;
  END IF;

  RETURN lenses.fn_cron_field_matches(v_parts[1], v_minute)
    AND lenses.fn_cron_field_matches(v_parts[2], v_hour)
    AND lenses.fn_cron_field_matches(v_parts[3], v_dom)
    AND lenses.fn_cron_field_matches(v_parts[4], v_month)
    AND (
      lenses.fn_cron_field_matches(v_parts[5], v_dow0)
      OR lenses.fn_cron_field_matches(v_parts[5], v_dow7)
    );
END;
$$;


ALTER FUNCTION "lenses"."fn_cron_matches_now"("p_cron_expr" "text", "p_now" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_cron_matches_now"("p_cron_expr" "text", "p_now" timestamp with time zone) IS 'Best-effort 5-field CRON matcher for minute-based workflow schedules. Supports *, comma lists, ranges, and step syntax.';



CREATE OR REPLACE FUNCTION "lenses"."fn_dispatch_scheduled_workflows"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'automation', 'public'
    AS $$
DECLARE
  v_now                    timestamptz := now();
  v_scheduled_for          timestamptz := date_trunc('minute', v_now);
  v_schedule               RECORD;
  v_run_id                 uuid;
  v_team_run_id            uuid;
  v_ai_lenser_id           uuid;
  v_assignee_ai_lenser_id  uuid;
  v_log_ai_lenser_id       uuid;
  v_requires_approval      boolean;
  v_team_run_status        text;
  v_approval_status        text;
  v_dispatched             integer := 0;
  -- budget enforcement
  v_spending_limit         numeric;
  v_budget_enforce         boolean;
  v_credits_used           numeric;
  -- Phase W
  v_condition_ctx          jsonb;
  v_condition_pass         boolean;
  v_rotation_len           integer;
  v_rotation_idx           integer;
  v_inputs                 jsonb;
BEGIN
  -- GUARD 1: system kill switch — halt all dispatch immediately.
  IF public.fn_kill_switch_active('system') THEN
    RETURN 0;
  END IF;

  -- GUARD 2: queue frozen — halt dispatch without cancelling in-flight runs.
  IF (SELECT queue_frozen FROM admin.execution_control WHERE id = 1) THEN
    RETURN 0;
  END IF;

  FOR v_schedule IN
    SELECT
      s.id,
      s.workflow_id,
      s.cron_expr,
      s.global_model_id,
      s.inputs_template,
      s.is_active,
      s.last_run_at,
      s.assignee_type,
      s.assignee_id,
      s.workflow_assignment_id,
      s.approval_policy,
      s.calendar_id,
      s.pre_dispatch_condition,
      s.inputs_rotation,
      s.last_rotation_index,
      w.lenser_id,
      w.title
    FROM lenses.workflow_schedules s
    JOIN lenses.workflows w ON w.id = s.workflow_id
    WHERE s.is_active = true
      AND (s.last_run_at IS NULL OR date_trunc('minute', s.last_run_at) < v_scheduled_for)
      AND lenses.fn_cron_matches_now(s.cron_expr, v_now)
    ORDER BY s.created_at ASC
    FOR UPDATE OF s SKIP LOCKED
  LOOP
    -- Resolve the workflow-owner AI lenser for action_logs fallback
    SELECT al.id INTO v_ai_lenser_id
    FROM agents.ai_lensers al
    WHERE al.profile_id = v_schedule.lenser_id
    LIMIT 1;

    -- Resolve the schedule assignee AI lenser for team_runs
    v_assignee_ai_lenser_id := NULL;
    IF v_schedule.assignee_id IS NOT NULL THEN
      IF v_schedule.assignee_type = 'agent' THEN
        v_assignee_ai_lenser_id := v_schedule.assignee_id;
      ELSIF v_schedule.assignee_type = 'team' THEN
        SELECT t.ai_lenser_id INTO v_assignee_ai_lenser_id
        FROM agents.teams t
        WHERE t.id = v_schedule.assignee_id
        LIMIT 1;
      END IF;
    END IF;

    v_log_ai_lenser_id := COALESCE(v_assignee_ai_lenser_id, v_ai_lenser_id);

    -- ── Budget enforcement gate ───────────────────────────────────────────
    IF v_ai_lenser_id IS NOT NULL THEN
      SELECT pol.spending_limit_credits, pol.budget_enforce
        INTO v_spending_limit, v_budget_enforce
      FROM agents.ai_lensers al
      JOIN agents.policies pol ON pol.id = al.policy_id
      WHERE al.id = v_ai_lenser_id;

      IF v_budget_enforce AND v_spending_limit IS NOT NULL THEN
        SELECT COALESCE(SUM(qs.credits_spent), 0)
          INTO v_credits_used
        FROM agents.quota_snapshots qs
        WHERE qs.ai_lenser_id = v_ai_lenser_id
          AND qs.period_date = CURRENT_DATE;

        IF v_credits_used >= v_spending_limit THEN
          UPDATE lenses.workflow_schedules
          SET
            last_dispatch_status = 'budget_exceeded',
            last_error_at = v_now,
            last_error_message = format(
              'spending_limit_credits=%s reached (used=%s today)',
              v_spending_limit, v_credits_used
            )
          WHERE id = v_schedule.id;

          IF v_log_ai_lenser_id IS NOT NULL THEN
            INSERT INTO agents.action_logs (
              ai_lenser_id, action_type, result, metadata
            ) VALUES (
              v_log_ai_lenser_id,
              'schedule_skipped',
              'budget_exceeded',
              jsonb_build_object(
                'reason', 'budget_exceeded',
                'schedule_id', v_schedule.id,
                'workflow_id', v_schedule.workflow_id,
                'scheduled_for', v_scheduled_for,
                'spending_limit_credits', v_spending_limit,
                'credits_used_today', v_credits_used
              )
            );
          END IF;

          CONTINUE;
        END IF;
      END IF;
    END IF;
    -- ── End budget gate ───────────────────────────────────────────────────

    -- Determine approval requirement from policy (default: requiresApproval=true)
    v_requires_approval := COALESCE(
      (v_schedule.approval_policy->>'requiresApproval')::boolean,
      true
    );

    IF lenses.fn_workflow_has_cycle(v_schedule.workflow_id) THEN
      UPDATE lenses.workflow_schedules
      SET
        last_dispatch_status = 'validation_failed',
        last_error_at = v_now,
        last_error_message = 'cycle_detected'
      WHERE id = v_schedule.id;

      IF v_log_ai_lenser_id IS NOT NULL THEN
        INSERT INTO agents.action_logs (
          ai_lenser_id, action_type, result, metadata
        ) VALUES (
          v_log_ai_lenser_id,
          'schedule_skipped',
          'failed',
          jsonb_build_object(
            'reason', 'cycle_detected',
            'schedule_id', v_schedule.id,
            'workflow_id', v_schedule.workflow_id,
            'scheduled_for', v_scheduled_for
          )
        );
      END IF;

      CONTINUE;
    END IF;

    IF EXISTS (
      SELECT 1
      FROM lenses.workflow_runs r
      WHERE r.schedule_id = v_schedule.id
        AND r.status IN ('draft', 'validated', 'queued', 'pending', 'running', 'streaming', 'recovered')
    ) THEN
      UPDATE lenses.workflow_schedules
      SET
        last_dispatch_status = 'skipped_overlap',
        last_error_at = v_now,
        last_error_message = 'overlap_in_flight'
      WHERE id = v_schedule.id;

      IF v_log_ai_lenser_id IS NOT NULL THEN
        INSERT INTO agents.action_logs (
          ai_lenser_id, action_type, result, metadata
        ) VALUES (
          v_log_ai_lenser_id,
          'schedule_skipped',
          'throttled',
          jsonb_build_object(
            'reason', 'overlap_in_flight',
            'schedule_id', v_schedule.id,
            'workflow_id', v_schedule.workflow_id,
            'scheduled_for', v_scheduled_for
          )
        );
      END IF;

      CONTINUE;
    END IF;

    -- ── Phase W1: calendar overlay guard ──────────────────────────────────
    IF NOT lenses.fn_check_calendar(v_schedule.calendar_id, v_now) THEN
      UPDATE lenses.workflow_schedules
      SET
        last_dispatch_status = 'skipped_calendar',
        last_error_at = v_now,
        last_error_message = 'calendar_skipped'
      WHERE id = v_schedule.id;

      IF v_log_ai_lenser_id IS NOT NULL THEN
        INSERT INTO agents.action_logs (
          ai_lenser_id, action_type, result, metadata
        ) VALUES (
          v_log_ai_lenser_id,
          'schedule_skipped',
          'skipped',
          jsonb_build_object(
            'schedule_id', v_schedule.id,
            'workflow_id', v_schedule.workflow_id,
            'scheduled_for', v_scheduled_for,
            'reason', 'calendar',
            'detail', 'calendar overlay denied this date',
            'calendar_id', v_schedule.calendar_id
          )
        );
      END IF;

      CONTINUE;
    END IF;
    -- ── End calendar guard ────────────────────────────────────────────────

    -- ── Phase W2: pre_dispatch_condition guard ────────────────────────────
    IF v_schedule.pre_dispatch_condition IS NOT NULL THEN
      v_condition_ctx := lenses.fn_build_schedule_condition_context(v_schedule.id);
      BEGIN
        v_condition_pass := automation.fn_eval_filter(
          v_schedule.pre_dispatch_condition,
          v_condition_ctx
        );
      EXCEPTION WHEN OTHERS THEN
        v_condition_pass := false;
      END;

      IF v_condition_pass IS DISTINCT FROM true THEN
        UPDATE lenses.workflow_schedules
        SET
          last_dispatch_status = 'skipped_condition',
          last_error_at = v_now,
          last_error_message = 'condition_failed'
        WHERE id = v_schedule.id;

        IF v_log_ai_lenser_id IS NOT NULL THEN
          INSERT INTO agents.action_logs (
            ai_lenser_id, action_type, result, metadata
          ) VALUES (
            v_log_ai_lenser_id,
            'schedule_skipped',
            'skipped',
            jsonb_build_object(
              'schedule_id', v_schedule.id,
              'workflow_id', v_schedule.workflow_id,
              'scheduled_for', v_scheduled_for,
              'reason', 'condition_failed',
              'detail', 'pre_dispatch_condition evaluated to false',
              'condition', v_schedule.pre_dispatch_condition,
              'context', v_condition_ctx
            )
          );
        END IF;

        CONTINUE;
      END IF;
    END IF;
    -- ── End condition guard ───────────────────────────────────────────────

    -- ── Phase W3: rotation pick ───────────────────────────────────────────
    IF v_schedule.inputs_rotation IS NOT NULL
       AND jsonb_typeof(v_schedule.inputs_rotation) = 'array'
       AND jsonb_array_length(v_schedule.inputs_rotation) > 0 THEN
      v_rotation_len := jsonb_array_length(v_schedule.inputs_rotation);
      v_rotation_idx := COALESCE(v_schedule.last_rotation_index, 0) % v_rotation_len;
      v_inputs := COALESCE(
        v_schedule.inputs_rotation -> v_rotation_idx,
        '{}'::jsonb
      );

      UPDATE lenses.workflow_schedules
      SET last_rotation_index = COALESCE(last_rotation_index, 0) + 1
      WHERE id = v_schedule.id;
    ELSE
      v_inputs := COALESCE(v_schedule.inputs_template, '{}'::jsonb);
    END IF;
    -- ── End rotation pick ─────────────────────────────────────────────────

    BEGIN
      INSERT INTO lenses.workflow_runs (
        workflow_id,
        triggered_by,
        status,
        context_inputs,
        global_model_id,
        schedule_id,
        scheduled_for,
        trigger_mode
      ) VALUES (
        v_schedule.workflow_id,
        v_schedule.lenser_id,
        'pending',
        v_inputs,
        v_schedule.global_model_id,
        v_schedule.id,
        v_scheduled_for,
        'schedule'
      )
      RETURNING id INTO v_run_id;

      INSERT INTO lenses.workflow_node_results (run_id, node_id, status)
      SELECT v_run_id, n.id, 'pending'
      FROM lenses.workflow_nodes n
      WHERE n.workflow_id = v_schedule.workflow_id;

      -- Create team_run and dispatch event when the schedule has an assignee
      IF v_assignee_ai_lenser_id IS NOT NULL THEN
        v_team_run_status  := CASE WHEN v_requires_approval THEN 'blocked' ELSE 'queued' END;
        v_approval_status  := CASE WHEN v_requires_approval THEN 'pending' ELSE 'not_required' END;

        INSERT INTO agents.team_runs (
          ai_lenser_id,
          team_id,
          workflow_id,
          workflow_run_id,
          workflow_assignment_id,
          status,
          approval_status,
          metadata
        ) VALUES (
          v_assignee_ai_lenser_id,
          CASE WHEN v_schedule.assignee_type = 'team' THEN v_schedule.assignee_id ELSE NULL END,
          v_schedule.workflow_id,
          v_run_id,
          v_schedule.workflow_assignment_id,
          v_team_run_status,
          v_approval_status,
          jsonb_build_object(
            'schedule_id', v_schedule.id,
            'scheduled_for', v_scheduled_for,
            'cron_expr', v_schedule.cron_expr
          )
        )
        RETURNING id INTO v_team_run_id;

        INSERT INTO agents.agent_run_events (
          team_run_id,
          event_type,
          payload,
          occurred_at
        ) VALUES (
          v_team_run_id,
          CASE WHEN v_requires_approval THEN 'approval_requested' ELSE 'dispatch_queued' END,
          jsonb_build_object(
            'schedule_id', v_schedule.id,
            'scheduled_for', v_scheduled_for,
            'cron_expr', v_schedule.cron_expr,
            'workflow_id', v_schedule.workflow_id,
            'workflow_run_id', v_run_id,
            'requires_approval', v_requires_approval
          ),
          v_now
        );
      END IF;

      UPDATE lenses.workflow_schedules
      SET
        last_run_at = v_now,
        last_run_id = v_run_id,
        last_dispatch_status = 'dispatched',
        last_error_at = NULL,
        last_error_message = NULL
      WHERE id = v_schedule.id;

      IF v_log_ai_lenser_id IS NOT NULL THEN
        INSERT INTO agents.action_logs (
          ai_lenser_id, action_type, result, metadata
        ) VALUES (
          v_log_ai_lenser_id,
          'dispatch_schedule',
          'success',
          jsonb_build_object(
            'schedule_id', v_schedule.id,
            'workflow_id', v_schedule.workflow_id,
            'workflow_title', v_schedule.title,
            'run_id', v_run_id,
            'team_run_id', v_team_run_id,
            'trigger_mode', 'schedule',
            'scheduled_for', v_scheduled_for,
            'assignee_type', v_schedule.assignee_type,
            'assignee_id', v_schedule.assignee_id,
            'workflow_assignment_id', v_schedule.workflow_assignment_id,
            'requires_approval', v_requires_approval,
            'rotation_index_used', CASE
              WHEN v_schedule.inputs_rotation IS NOT NULL
                AND jsonb_typeof(v_schedule.inputs_rotation) = 'array'
                AND jsonb_array_length(v_schedule.inputs_rotation) > 0
              THEN COALESCE(v_schedule.last_rotation_index, 0)
                   % jsonb_array_length(v_schedule.inputs_rotation)
              ELSE NULL
            END
          )
        );
      END IF;

      v_dispatched := v_dispatched + 1;
    EXCEPTION
      WHEN unique_violation THEN
        UPDATE lenses.workflow_schedules
        SET
          last_dispatch_status = 'skipped_overlap',
          last_error_at = v_now,
          last_error_message = 'schedule_slot_exists'
        WHERE id = v_schedule.id;

        IF v_log_ai_lenser_id IS NOT NULL THEN
          INSERT INTO agents.action_logs (
            ai_lenser_id, action_type, result, metadata
          ) VALUES (
            v_log_ai_lenser_id,
            'schedule_skipped',
            'throttled',
            jsonb_build_object(
              'reason', 'schedule_slot_exists',
              'schedule_id', v_schedule.id,
              'workflow_id', v_schedule.workflow_id,
              'scheduled_for', v_scheduled_for
            )
          );
        END IF;
      WHEN OTHERS THEN
        UPDATE lenses.workflow_schedules
        SET
          last_dispatch_status = 'dispatch_failed',
          last_error_at = v_now,
          last_error_message = left(SQLERRM, 500)
        WHERE id = v_schedule.id;

        IF v_log_ai_lenser_id IS NOT NULL THEN
          INSERT INTO agents.action_logs (
            ai_lenser_id, action_type, result, metadata
          ) VALUES (
            v_log_ai_lenser_id,
            'dispatch_schedule',
            'failed',
            jsonb_build_object(
              'schedule_id', v_schedule.id,
              'workflow_id', v_schedule.workflow_id,
              'scheduled_for', v_scheduled_for,
              'error', left(SQLERRM, 500)
            )
          );
        END IF;
    END;
  END LOOP;

  RETURN v_dispatched;
END;
$$;


ALTER FUNCTION "lenses"."fn_dispatch_scheduled_workflows"() OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_dispatch_scheduled_workflows"() IS 'Phase W + Emergency Stop: dispatch loop with calendar overlay (W1), pre_dispatch_condition (W2), parameter rotation (W3), plus kill-switch and queue-freeze guards at entry. Returns 0 immediately when either guard is active, preventing any new runs from being enqueued.';



CREATE OR REPLACE FUNCTION "lenses"."fn_dispatch_scheduled_workflows_safe"() RETURNS "void"
    LANGUAGE "sql"
    SET "search_path" TO 'lenses', 'automation', 'pg_catalog'
    AS $$
        SELECT automation.fn_run_with_lock(
          'dispatch-scheduled-workflows',
          'SELECT lenses.fn_dispatch_scheduled_workflows()'
        );
      $$;


ALTER FUNCTION "lenses"."fn_dispatch_scheduled_workflows_safe"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_get_version_params_with_tools"("p_version_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT COALESCE(
    jsonb_agg(
      jsonb_build_object(
        'id',        vp.id,
        'version_id', vp.version_id,
        'label',     vp.label,
        'tool_id',   vp.tool_id,
        'tool', jsonb_build_object(
          'id',               t.id,
          'key',              t.key,
          'label',            t.label,
          'description',      t.description,
          'category',         t.category,
          'type',             t.type,
          'required',         t.required,
          'min_length',       t.min_length,
          'max_length',       t.max_length,
          'placeholder',      t.placeholder,
          'help_text',        t.help_text,
          'validation_schema', t.validation_schema,
          'options',          t.options,
          'sort_order',       t.sort_order,
          'is_system',        t.is_system,
          'icon',             t.icon,
          'color',            t.color
        )
      )
      ORDER BY t.sort_order, vp.label
    ),
    '[]'::jsonb
  )
  FROM  lenses.version_parameters vp
  JOIN  lenses.tools t ON t.id = vp.tool_id
  WHERE vp.version_id = p_version_id;
$$;


ALTER FUNCTION "lenses"."fn_get_version_params_with_tools"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_get_workflow_versions"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "version_number" integer, "changelog" "text", "status" "text", "published_at" timestamp with time zone, "created_by" "uuid", "created_at" timestamp with time zone, "node_count" bigint, "edge_count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT
    wv.id,
    wv.workflow_id,
    wv.version_number,
    wv.changelog,
    wv.status,
    wv.published_at,
    wv.created_by,
    wv.created_at,
    (SELECT count(*) FROM lenses.workflow_version_nodes n WHERE n.workflow_version_id = wv.id) AS node_count,
    (SELECT count(*) FROM lenses.workflow_version_edges e WHERE e.workflow_version_id = wv.id) AS edge_count
  FROM lenses.workflow_versions wv
  WHERE wv.workflow_id = p_workflow_id
  ORDER BY wv.version_number DESC;
$$;


ALTER FUNCTION "lenses"."fn_get_workflow_versions"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_list_lens_versions"("p_lens_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lens_id" "uuid", "version_number" integer, "status" "text", "changelog" "text", "parameter_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses'
    AS $$
  SELECT
    v.id,
    v.lens_id,
    v.version_number,
    v.status::text,
    v.changelog,
    COUNT(vp.id)::integer AS parameter_count,
    v.created_at
  FROM lenses.versions v
  LEFT JOIN lenses.version_parameters vp ON vp.version_id = v.id
  WHERE v.lens_id = p_lens_id
  GROUP BY v.id
  ORDER BY v.version_number DESC
  LIMIT  LEAST(p_limit, 50)
  OFFSET GREATEST(p_offset, 0)
$$;


ALTER FUNCTION "lenses"."fn_list_lens_versions"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_list_tools"("p_category" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT COALESCE(
    jsonb_agg(
      jsonb_build_object(
        'id',               id,
        'key',              key,
        'label',            label,
        'description',      description,
        'category',         category,
        'type',             type,
        'required',         required,
        'min_length',       min_length,
        'max_length',       max_length,
        'placeholder',      placeholder,
        'help_text',        help_text,
        'validation_schema', validation_schema,
        'options',          options,
        'sort_order',       sort_order,
        'is_system',        is_system,
        'icon',             icon,
        'color',            color
      )
      ORDER BY sort_order, key
    ),
    '[]'::jsonb
  )
  FROM lenses.tools
  WHERE (p_category IS NULL OR category = p_category);
$$;


ALTER FUNCTION "lenses"."fn_list_tools"("p_category" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_list_versions"("p_lens_id" "uuid") RETURNS SETOF "lenses"."versions"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses'
    AS $$
  SELECT *
  FROM lenses.versions
  WHERE lens_id = p_lens_id
  ORDER BY version_number DESC;
$$;


ALTER FUNCTION "lenses"."fn_list_versions"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_list_versions"("p_lens_id" "uuid") IS 'DEPRECATED: use lenses.fn_list_versions_paged(p_lens_id, p_limit, p_cursor).';



CREATE OR REPLACE FUNCTION "lenses"."fn_list_versions_paged"("p_lens_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "lenses"."versions"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
        SELECT *
        FROM lenses.versions v
        WHERE v.lens_id = p_lens_id
          AND (p_cursor IS NULL OR v.created_at < p_cursor)
        ORDER BY v.created_at DESC
        LIMIT LEAST(GREATEST(coalesce(p_limit, 50), 1), 100)
      $$;


ALTER FUNCTION "lenses"."fn_list_versions_paged"("p_lens_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_move_workflow_run_to_dlq"("p_run_id" "uuid", "p_node_id" "text" DEFAULT NULL::"text", "p_error_code" "text" DEFAULT NULL::"text", "p_error_msg" "text" DEFAULT NULL::"text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$
DECLARE
  v_dlq_id UUID;
BEGIN
  INSERT INTO lenses.workflow_run_dead_letters
    (run_id, node_id, error_code, error_message, payload)
  VALUES
    (p_run_id, p_node_id, p_error_code, p_error_msg, p_payload)
  RETURNING id INTO v_dlq_id;

  RETURN v_dlq_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_move_workflow_run_to_dlq"("p_run_id" "uuid", "p_node_id" "text", "p_error_code" "text", "p_error_msg" "text", "p_payload" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_prevent_published_version_update"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses'
    AS $$
BEGIN
  IF OLD.status = 'published' THEN
    RAISE EXCEPTION
      'Published versions are immutable (id=%). Create a new draft instead.',
      OLD.id
      USING ERRCODE = 'check_violation';
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_prevent_published_version_update"() OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_prevent_published_version_update"() IS 'Trigger guard: raises an exception when any column of a published lenses.versions row
   is updated. Published versions must remain immutable; all edits must go through
   fn_create_draft_version to create a new append-only draft row.';



CREATE OR REPLACE FUNCTION "lenses"."fn_preview_schedule_ticks"("p_schedule_id" "uuid", "p_n" integer DEFAULT 10) RETURNS TABLE("tick_at" timestamp with time zone, "decision" "text", "reason" "text", "inputs" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'automation', 'pg_catalog'
    AS $$
DECLARE
  v_n           int := GREATEST(1, LEAST(COALESCE(p_n, 10), 200));
  v_horizon_min int := 60 * 24 * 31; -- look ahead at most ~31 days
  v_schedule    RECORD;
  v_ctx         jsonb;
  v_cursor      timestamptz := date_trunc('minute', now()) + interval '1 minute';
  v_emitted     int := 0;
  v_step        int := 0;
  v_pass        boolean;
  v_rotation_len int;
  v_rotation_idx int;
  v_inputs      jsonb;
  v_decision    text;
  v_reason      text;
BEGIN
  SELECT
    s.id,
    s.workflow_id,
    s.cron_expr,
    s.inputs_template,
    s.calendar_id,
    s.pre_dispatch_condition,
    s.inputs_rotation,
    s.last_rotation_index,
    w.lenser_id
  INTO v_schedule
  FROM lenses.workflow_schedules s
  JOIN lenses.workflows w ON w.id = s.workflow_id
  WHERE s.id = p_schedule_id;

  IF NOT FOUND THEN
    RETURN;
  END IF;

  -- Owner-only access for the preview. service_role calls present a NULL
  -- get_auth_lenser_id() and bypass the check below; UI calls (authenticated
  -- via PostgREST) must own the schedule.
  IF lensers.get_auth_lenser_id() IS NOT NULL
     AND v_schedule.lenser_id IS DISTINCT FROM lensers.get_auth_lenser_id() THEN
    RAISE EXCEPTION 'Schedule not found or not owned by the active workspace'
      USING ERRCODE = '42501';
  END IF;

  v_ctx := lenses.fn_build_schedule_condition_context(p_schedule_id);

  WHILE v_emitted < v_n AND v_step < v_horizon_min LOOP
    v_step := v_step + 1;
    IF NOT lenses.fn_cron_matches_now(v_schedule.cron_expr, v_cursor) THEN
      v_cursor := v_cursor + interval '1 minute';
      CONTINUE;
    END IF;

    -- Guard 1: calendar.
    IF NOT lenses.fn_check_calendar(v_schedule.calendar_id, v_cursor) THEN
      v_decision := 'skip';
      v_reason   := 'calendar';
      v_inputs   := NULL;
    ELSE
      -- Guard 2: condition.
      v_pass := true;
      IF v_schedule.pre_dispatch_condition IS NOT NULL THEN
        BEGIN
          v_pass := automation.fn_eval_filter(v_schedule.pre_dispatch_condition, v_ctx);
        EXCEPTION WHEN OTHERS THEN
          v_pass := false;
        END;
      END IF;

      IF v_pass IS DISTINCT FROM true THEN
        v_decision := 'skip';
        v_reason   := 'condition_failed';
        v_inputs   := NULL;
      ELSE
        -- Guard 3: rotation pick (preview-only — does NOT mutate counter).
        IF v_schedule.inputs_rotation IS NOT NULL
           AND jsonb_typeof(v_schedule.inputs_rotation) = 'array'
           AND jsonb_array_length(v_schedule.inputs_rotation) > 0 THEN
          v_rotation_len := jsonb_array_length(v_schedule.inputs_rotation);
          v_rotation_idx := (COALESCE(v_schedule.last_rotation_index, 0) + v_emitted) % v_rotation_len;
          v_inputs := COALESCE(v_schedule.inputs_rotation -> v_rotation_idx, '{}'::jsonb);
        ELSE
          v_inputs := COALESCE(v_schedule.inputs_template, '{}'::jsonb);
        END IF;

        v_decision := 'dispatch';
        v_reason   := 'ok';
      END IF;
    END IF;

    tick_at  := v_cursor;
    decision := v_decision;
    reason   := v_reason;
    inputs   := v_inputs;
    RETURN NEXT;

    v_emitted := v_emitted + 1;
    v_cursor  := v_cursor + interval '1 minute';
  END LOOP;

  RETURN;
END;
$$;


ALTER FUNCTION "lenses"."fn_preview_schedule_ticks"("p_schedule_id" "uuid", "p_n" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_preview_schedule_ticks"("p_schedule_id" "uuid", "p_n" integer) IS 'Phase W: dry-run helper. Steps forward minute-by-minute, returns the next N ticks whose cron matches and labels each as dispatch/skip with a reason. Uses fn_cron_matches_now as the schedule oracle (no cron.next_run helper is exposed by Supabase pg_cron). prior_run_result and last_24h_stats are snapshotted once and reused across all preview ticks.';



CREATE OR REPLACE FUNCTION "lenses"."fn_publish_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'content', 'public'
    AS $$
DECLARE
  v_owner         uuid;
  v_ver_status    "content"."content_status";
  v_template_body text;
  v_lens_id       uuid;
BEGIN
  SELECT "p"."lenser_id", "v"."status", "v"."template_body", "v"."lens_id"
  INTO   v_owner, v_ver_status, v_template_body, v_lens_id
  FROM   "lenses"."versions" v
  JOIN   "lenses"."lenses"   p ON p."id" = v."lens_id"
  WHERE  v."id" = p_version_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Version not found: %', p_version_id
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_ver_status <> 'draft' THEN
    RAISE EXCEPTION 'Version % is already in status %. Only draft versions can be published.',
      p_version_id, v_ver_status
      USING ERRCODE = 'invalid_parameter_value';
  END IF;

  IF v_owner <> "lensers"."get_auth_lenser_id"() THEN
    RAISE EXCEPTION 'Permission denied: you do not own this lens version'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF length(trim(v_template_body)) < 50 THEN
    RAISE EXCEPTION 'Cannot publish version %: template_body must be at least 50 characters (got %).',
      p_version_id, length(trim(v_template_body))
      USING ERRCODE = 'invalid_parameter_value';
  END IF;

  -- Publish the version
  UPDATE "lenses"."versions"
  SET  "status"       = 'published',
       "published_at" = now()
  WHERE "id" = p_version_id;

  -- Advance HEAD to the newly published version
  UPDATE "lenses"."lenses"
  SET  "head_version_id" = p_version_id
  WHERE "id" = v_lens_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_publish_version"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_publish_version"("p_version_id" "uuid") IS 'Publishes a draft lens version (draft → published). Validates ownership, draft status, and template_body minimum length. Advances lenses.head_version_id to the newly published version. SECURITY DEFINER: ownership checked internally.';



CREATE OR REPLACE FUNCTION "lenses"."fn_publish_workflow_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id     uuid;
  v_workflow_id   uuid;
  v_owner_id      uuid;
  v_current_status text;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Get version info
  SELECT wv.workflow_id, wv.status INTO v_workflow_id, v_current_status
  FROM lenses.workflow_versions wv
  WHERE wv.id = p_version_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Version not found'
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_current_status <> 'draft' THEN
    RAISE EXCEPTION 'Only draft versions can be published'
      USING ERRCODE = 'check_violation';
  END IF;

  -- Ownership check
  SELECT lenser_id INTO v_owner_id
  FROM lenses.workflows
  WHERE id = v_workflow_id;

  IF v_owner_id <> v_caller_id THEN
    RAISE EXCEPTION 'Only the workflow owner can publish versions'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Archive previous published version(s)
  UPDATE lenses.workflow_versions
  SET status = 'archived'
  WHERE workflow_id = v_workflow_id
    AND status = 'published';

  -- Publish this version
  UPDATE lenses.workflow_versions
  SET status = 'published', published_at = now()
  WHERE id = p_version_id;

  -- Update head_version_id on workflow
  UPDATE lenses.workflows
  SET head_version_id = p_version_id, updated_at = now()
  WHERE id = v_workflow_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_publish_workflow_version"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
    v_template  text;
    v_rendered  text;
    v_param     record;
    v_val       text;
BEGIN
    -- Fetch the stored template body (may be [[label]] or legacy [[:uuid]] format)
    SELECT template_body
      INTO STRICT v_template
      FROM lenses.versions
     WHERE id = p_version_id;

    v_rendered := v_template;

    -- Join version_parameters → tools to get label, type, required.
    -- Use parameter label as the input key (consistent with fn_create_lens and
    -- fn_validate_inputs as fixed in 20270202000000_fix_version_parameters_column_refs).
    FOR v_param IN
        SELECT vp.id,
               vp.label,
               t.type,
               t.required
          FROM lenses.version_parameters vp
          JOIN lenses.tools t ON t.id = vp.tool_id
         WHERE vp.version_id = p_version_id
         ORDER BY vp.label
    LOOP
        -- Step 1: convert legacy [[:uuid]] token → [[label]] if still present.
        -- This handles rows created via fn_create_lens before this fix.
        v_rendered := replace(
            v_rendered,
            '[[:'|| v_param.id::text ||']]',
            '[[' || v_param.label || ']]'
        );

        -- Step 2: resolve caller-supplied value for this label
        v_val := NULLIF(trim(p_inputs ->> v_param.label), '');

        -- Enforce required parameters
        IF v_val IS NULL AND v_param.required THEN
            RAISE EXCEPTION
                'Required parameter ''%'' is missing or empty',
                v_param.label
                USING ERRCODE = '23514'; -- check_violation
        END IF;

        -- Step 3: substitute [[label]] with the resolved value
        v_rendered := replace(
            v_rendered,
            '[[' || v_param.label || ']]',
            coalesce(v_val, '')
        );
    END LOOP;

    RETURN v_rendered;

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE EXCEPTION 'Version % not found', p_version_id
            USING ERRCODE = 'P0002';
END;
$$;


ALTER FUNCTION "lenses"."fn_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") IS 'Renders a lens version template_body by substituting [[label]] tokens with
 caller-supplied inputs (p_inputs jsonb keyed by parameter label). Also handles
 legacy [[:uuid]] bodies by converting them to [[label]] first. Joins
 lenses.version_parameters → lenses.tools to resolve type and required.
 Raises 23514 on missing required params. Called by fn_worker_render_template.
 Fixed in 20271231000000: removed refs to non-existent version_parameters columns
 (key, type, required, default_value, sort_order). SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "lenses"."fn_render_version_body"("p_version_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_body  text;
  v_param RECORD;
BEGIN
  SELECT template_body INTO v_body
  FROM   lenses.versions
  WHERE  id = p_version_id;

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  FOR v_param IN
    SELECT id, label
    FROM   lenses.version_parameters
    WHERE  version_id = p_version_id
  LOOP
    v_body := replace(
      v_body,
      '[[:'|| v_param.id::text ||']]',
      '[[' || v_param.label || ']]'
    );
  END LOOP;

  RETURN v_body;
END;
$$;


ALTER FUNCTION "lenses"."fn_render_version_body"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_restore_workflow_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id     uuid;
  v_workflow_id   uuid;
  v_owner_id      uuid;
  v_node_map      jsonb := '{}';
  v_old_node      record;
  v_new_node_id   uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Get version info
  SELECT wv.workflow_id INTO v_workflow_id
  FROM lenses.workflow_versions wv
  WHERE wv.id = p_version_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Version not found'
      USING ERRCODE = 'no_data_found';
  END IF;

  -- Ownership check
  SELECT lenser_id INTO v_owner_id
  FROM lenses.workflows
  WHERE id = v_workflow_id;

  IF v_owner_id <> v_caller_id THEN
    RAISE EXCEPTION 'Only the workflow owner can restore versions'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Delete current live nodes and edges (cascade will handle edges via workflow_id)
  DELETE FROM lenses.workflow_edges WHERE workflow_id = v_workflow_id;
  DELETE FROM lenses.workflow_nodes WHERE workflow_id = v_workflow_id;

  -- Copy version nodes back to live nodes
  FOR v_old_node IN
    SELECT id, lens_id, version_id, position_x, position_y, label, ordinal, config
    FROM lenses.workflow_version_nodes
    WHERE workflow_version_id = p_version_id
    ORDER BY ordinal
  LOOP
    v_new_node_id := gen_random_uuid();

    INSERT INTO lenses.workflow_nodes (
      id, workflow_id, lens_id, version_id,
      position_x, position_y, label, ordinal, config
    ) VALUES (
      v_new_node_id, v_workflow_id, v_old_node.lens_id, v_old_node.version_id,
      v_old_node.position_x, v_old_node.position_y, v_old_node.label,
      v_old_node.ordinal, v_old_node.config
    );

    v_node_map := v_node_map || jsonb_build_object(v_old_node.id::text, v_new_node_id::text);
  END LOOP;

  -- Copy version edges back to live edges
  INSERT INTO lenses.workflow_edges (
    workflow_id, source_node_id, target_node_id,
    source_output_key, target_param_label
  )
  SELECT
    v_workflow_id,
    (v_node_map ->> e.source_node_id::text)::uuid,
    (v_node_map ->> e.target_node_id::text)::uuid,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_version_edges e
  WHERE e.workflow_version_id = p_version_id;

  -- Update workflow timestamp
  UPDATE lenses.workflows
  SET updated_at = now()
  WHERE id = v_workflow_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_restore_workflow_version"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_schedule_calendars_touch_updated"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_schedule_calendars_touch_updated"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$ SELECT lenses.fn_start_workflow_run(p_workflow_id, p_inputs, NULL::text, NULL::text); $$;


ALTER FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb") IS 'Creates a workflow_run and seeds pending workflow_node_results for every node. Returns the run_id. The CF Worker polls/subscribes to drive actual execution.';



CREATE OR REPLACE FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_global_model_id" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$ SELECT lenses.fn_start_workflow_run(p_workflow_id, p_inputs, p_global_model_id, NULL::text); $$;


ALTER FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text") IS 'Creates a workflow_run with optional global_model_id and seeds pending workflow_node_results for every node. Returns the run_id. The CF Worker polls/subscribes to drive actual execution.';



CREATE OR REPLACE FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_global_model_id" "text" DEFAULT NULL::"text", "p_idempotency_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id        UUID;
  v_run_id           UUID;
  v_rate_window_sec  INTEGER := 60;
  v_rate_limit_count INTEGER := 30;
  v_recent_count     INTEGER;
  v_role             TEXT;
  v_version_id       UUID;
  v_snapshot_hash    TEXT;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id
      AND w.archived_at IS NULL
      AND w.deleted_at IS NULL
      AND (w.visibility = 'public' OR w.lenser_id = v_lenser_id OR public.fn_lifecycle_is_service_role())
  ) THEN
    RAISE EXCEPTION 'workflow_not_found_or_forbidden'
      USING ERRCODE = '42501';
  END IF;

  IF p_idempotency_key IS NOT NULL AND length(p_idempotency_key) > 0 THEN
    SELECT id INTO v_run_id
    FROM lenses.workflow_runs
    WHERE workflow_id = p_workflow_id
      AND idempotency_key = p_idempotency_key
      AND (idempotency_expires_at IS NULL OR idempotency_expires_at > now())
    ORDER BY created_at DESC
    LIMIT 1;
    IF v_run_id IS NOT NULL THEN
      RETURN v_run_id;
    END IF;
  END IF;

  BEGIN
    v_role := current_setting('request.jwt.claim.role', true);
  EXCEPTION WHEN OTHERS THEN
    v_role := NULL;
  END;

  IF v_lenser_id IS NULL THEN
    IF v_role = 'anon' THEN
      RAISE EXCEPTION 'fn_start_workflow_run: anon not permitted'
        USING ERRCODE = '42501',
              HINT    = 'rate limit cannot be enforced for anon';
    END IF;
  ELSE
    v_recent_count := lenses.fn_count_recent_runs(v_lenser_id, v_rate_window_sec);
    IF v_recent_count >= v_rate_limit_count THEN
      RAISE EXCEPTION
        'rate_limited: % runs in the last % seconds (cap %)',
        v_recent_count, v_rate_window_sec, v_rate_limit_count
        USING ERRCODE = '54000', HINT = 'workflow_run_rate_limit';
    END IF;
  END IF;

  v_version_id := lenses.fn_create_workflow_execution_snapshot(p_workflow_id, v_lenser_id);

  SELECT snapshot_hash INTO v_snapshot_hash
  FROM lenses.workflow_versions
  WHERE id = v_version_id;

  INSERT INTO lenses.workflow_runs (
    workflow_id,
    workflow_version_id,
    workflow_snapshot_hash,
    triggered_by,
    status,
    context_inputs,
    global_model_id,
    idempotency_key,
    idempotency_expires_at
  )
  VALUES (
    p_workflow_id,
    v_version_id,
    v_snapshot_hash,
    v_lenser_id,
    'pending',
    p_inputs,
    p_global_model_id,
    p_idempotency_key,
    CASE
      WHEN p_idempotency_key IS NOT NULL AND length(p_idempotency_key) > 0
      THEN now() + interval '24 hours'
      ELSE NULL
    END
  )
  RETURNING id INTO v_run_id;

  INSERT INTO lenses.workflow_node_results (
    run_id,
    node_id,
    workflow_version_node_id,
    resolved_lens_version_id,
    node_snapshot_hash,
    status
  )
  SELECT
    v_run_id,
    n.source_node_id,
    n.id,
    n.version_id,
    encode(extensions.digest(
      jsonb_build_object(
        'workflow_version_node_id', n.id,
        'lens_id', n.lens_id,
        'version_id', n.version_id,
        'config', n.config
      )::text,
      'sha256'
    ), 'hex'),
    'pending'
  FROM lenses.workflow_version_nodes n
  WHERE n.workflow_version_id = v_version_id
    AND n.source_node_id IS NOT NULL;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text") IS 'D2+D4: anon callers rejected (cannot bypass per-lenser rate limit); idempotency_key window capped at 24h via idempotency_expires_at. Otherwise mirrors phase 9.';



CREATE OR REPLACE FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_global_model_id" "text" DEFAULT NULL::"text", "p_idempotency_key" "text" DEFAULT NULL::"text", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id        uuid;
  v_run_id           uuid;
  v_rate_window_sec  integer := 60;
  v_rate_limit_count integer := 30;
  v_recent_count     integer;
  v_role             text;
  v_resolved_version uuid;
  v_head_version     uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id
      AND (w.visibility = 'public' OR w.lenser_id = v_lenser_id)
  ) THEN
    RAISE EXCEPTION 'workflow_not_found_or_forbidden'
      USING ERRCODE = '42501';
  END IF;

  -- ── Idempotency short-circuit with TTL ────────────────────────────────────
  IF p_idempotency_key IS NOT NULL AND length(p_idempotency_key) > 0 THEN
    SELECT id INTO v_run_id
    FROM   lenses.workflow_runs
    WHERE  workflow_id     = p_workflow_id
      AND  idempotency_key = p_idempotency_key
      AND  (idempotency_expires_at IS NULL OR idempotency_expires_at > now())
    ORDER  BY created_at DESC
    LIMIT  1;
    IF v_run_id IS NOT NULL THEN
      RETURN v_run_id;
    END IF;
  END IF;

  -- ── Rate limit (anon blocked; authenticated bucketed per-lenser) ──────────
  BEGIN
    v_role := current_setting('request.jwt.claim.role', true);
  EXCEPTION WHEN OTHERS THEN
    v_role := NULL;
  END;

  IF v_lenser_id IS NULL THEN
    IF v_role = 'anon' THEN
      RAISE EXCEPTION 'fn_start_workflow_run: anon not permitted'
        USING ERRCODE = '42501',
              HINT    = 'D2: rate limit cannot be enforced for anon';
    END IF;
    -- service_role callers (workers/test) bypass the limit intentionally.
  ELSE
    v_recent_count := lenses.fn_count_recent_runs(v_lenser_id, v_rate_window_sec);
    IF v_recent_count >= v_rate_limit_count THEN
      RAISE EXCEPTION
        'rate_limited: % runs in the last % seconds (cap %)',
        v_recent_count, v_rate_window_sec, v_rate_limit_count
        USING ERRCODE = '54000', HINT = 'phase9_run_rate_limit';
    END IF;
  END IF;

  -- ── Version resolution ────────────────────────────────────────────────────
  -- Caller may pass an explicit version_id (e.g. to pin a specific published
  -- version or to dry-run a draft).  When absent, fall back to the workflow's
  -- head_version_id (the most recently published version).  A NULL result
  -- means the workflow has never had a published version — the run proceeds
  -- without version binding for backward compatibility.
  IF p_version_id IS NOT NULL THEN
    -- Validate the version belongs to this workflow.
    IF NOT EXISTS (
      SELECT 1 FROM lenses.workflow_versions wv
      WHERE wv.id = p_version_id AND wv.workflow_id = p_workflow_id
    ) THEN
      RAISE EXCEPTION 'version_not_found_or_wrong_workflow'
        USING ERRCODE = '42501';
    END IF;
    v_resolved_version := p_version_id;
  ELSE
    SELECT head_version_id INTO v_head_version
    FROM   lenses.workflows
    WHERE  id = p_workflow_id;
    v_resolved_version := v_head_version;  -- may be NULL for unversioned workflows
  END IF;

  -- ── Create the run ────────────────────────────────────────────────────────
  INSERT INTO lenses.workflow_runs (
    workflow_id, triggered_by, status, context_inputs,
    global_model_id, idempotency_key, idempotency_expires_at,
    workflow_version_id
  )
  VALUES (
    p_workflow_id, v_lenser_id, 'pending', p_inputs,
    p_global_model_id, p_idempotency_key,
    CASE
      WHEN p_idempotency_key IS NOT NULL AND length(p_idempotency_key) > 0
      THEN now() + interval '24 hours'
      ELSE NULL
    END,
    v_resolved_version
  )
  RETURNING id INTO v_run_id;

  -- Seed one node_result row per node (all pending)
  INSERT INTO lenses.workflow_node_results (run_id, node_id, status)
  SELECT v_run_id, n.id, 'pending'
  FROM   lenses.workflow_nodes n
  WHERE  n.workflow_id = p_workflow_id;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") IS 'Creates a workflow_run, seeds pending node_results, and binds workflow_version_id. When p_version_id is NULL, resolves to head_version_id (latest published version). Preserves D2 anon-block and D4 idempotency TTL guards. Runs against unversioned workflows (no head_version_id) proceed with NULL version for backward compat.';



CREATE OR REPLACE FUNCTION "lenses"."fn_touch_workflow_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_touch_workflow_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_trg_notify_lens_featured"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  PERFORM public.fn_insert_notification(
    NEW.lenser_id,
    'lens_featured',
    'Your lens was featured!',
    'Congratulations — your lens has been selected as a featured lens.',
    '/lenses/' || NEW.id,
    jsonb_build_object('lens_id', NEW.id)
    -- no p_actor_id: admin action, skip block/mute gate
  );

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_lens_featured failed for lens %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_trg_notify_lens_featured"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_trg_notify_lens_forked"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_parent_owner_id     UUID;
  v_parent_vis          content.visibility_enum;
  v_forker_display_name TEXT;
  v_forker_handle       TEXT;
  v_parent_title        TEXT;
  v_notif_id            UUID;
BEGIN
  SELECT l.lenser_id, l.visibility
    INTO v_parent_owner_id, v_parent_vis
    FROM lenses.lenses l
   WHERE l.id = NEW.parent_lens_id;

  IF NOT FOUND OR v_parent_vis = 'private'::content.visibility_enum THEN
    RETURN NEW;
  END IF;

  IF NOT public.fn_check_and_upsert_aggregate(
    v_parent_owner_id, NEW.lenser_id, 'lens_forked', NEW.parent_lens_id, '1 hour'
  ) THEN
    RETURN NEW;
  END IF;

  SELECT display_name, handle
    INTO v_forker_display_name, v_forker_handle
    FROM lensers.profiles
   WHERE id = NEW.lenser_id;

  SELECT et.title
    INTO v_parent_title
    FROM content.entity_translations et
   WHERE et.entity_id   = NEW.parent_lens_id
     AND et.entity_type = 'lens'::content.entity_type_enum
     AND et.is_original = true
   LIMIT 1;

  v_notif_id := public.fn_insert_notification(
    v_parent_owner_id,
    'lens_forked',
    COALESCE(v_forker_display_name, v_forker_handle) || ' forked your lens',
    COALESCE(v_parent_title, 'Your lens'),
    '/lenses/' || NEW.id,
    jsonb_build_object(
      'forker_id',           NEW.lenser_id,
      'forker_handle',       v_forker_handle,
      'forker_display_name', v_forker_display_name,
      'parent_lens_id',      NEW.parent_lens_id,
      'fork_lens_id',        NEW.id
    ),
    NEW.lenser_id
  );

  IF v_notif_id IS NOT NULL THEN
    UPDATE public.notification_aggregates
       SET notification_id = v_notif_id
     WHERE recipient_id      = v_parent_owner_id
       AND notification_type = 'lens_forked'
       AND entity_id         = NEW.parent_lens_id
       AND notification_id   IS NULL
       AND window_end        > now();
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_lens_forked failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_trg_notify_lens_forked"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_trg_notify_lens_published"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_author_display_name TEXT;
  v_author_handle       TEXT;
  v_title               TEXT;
  v_follower            RECORD;
BEGIN
  IF NEW.visibility = 'private'::content.visibility_enum THEN
    RETURN NEW;
  END IF;

  SELECT display_name, handle
    INTO v_author_display_name, v_author_handle
    FROM lensers.profiles
   WHERE id = NEW.lenser_id;

  SELECT et.title
    INTO v_title
    FROM content.entity_translations et
   WHERE et.entity_id   = NEW.id
     AND et.entity_type = 'lens'::content.entity_type_enum
     AND et.is_original = true
   LIMIT 1;

  -- Fan-out to accepted followers.
  -- Safety cap: 5000 synchronous notifications per publish.
  -- Accounts with > 5000 followers require async worker fan-out migration.
  FOR v_follower IN
    SELECT r.source_profile_id AS follower_id
      FROM lensers.relationships r
     WHERE r.target_profile_id = NEW.lenser_id
       AND r.status            = 'accepted'
       AND r.removed_at        IS NULL
     LIMIT 5000
  LOOP
    PERFORM public.fn_insert_notification(
      v_follower.follower_id,
      'lens_published',
      COALESCE(v_author_display_name, v_author_handle) || ' published a new lens',
      v_title,
      '/lenses/' || NEW.id,
      jsonb_build_object(
        'author_id',     NEW.lenser_id,
        'author_handle', v_author_handle,
        'lens_id',       NEW.id,
        'lens_title',    v_title,
        'visibility',    NEW.visibility::text
      ),
      NEW.lenser_id
    );
  END LOOP;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_lens_published failed for lens %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_trg_notify_lens_published"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_trg_notify_workflow_forked"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_parent_owner_id     UUID;
  v_parent_vis          TEXT;
  v_forker_display_name TEXT;
  v_forker_handle       TEXT;
  v_notif_id            UUID;
BEGIN
  SELECT w.lenser_id, w.visibility
    INTO v_parent_owner_id, v_parent_vis
    FROM lenses.workflows w
   WHERE w.id = NEW.parent_workflow_id;

  IF NOT FOUND OR v_parent_vis = 'private' THEN
    RETURN NEW;
  END IF;

  IF NOT public.fn_check_and_upsert_aggregate(
    v_parent_owner_id, NEW.lenser_id, 'workflow_forked', NEW.parent_workflow_id, '1 hour'
  ) THEN
    RETURN NEW;
  END IF;

  SELECT display_name, handle
    INTO v_forker_display_name, v_forker_handle
    FROM lensers.profiles
   WHERE id = NEW.lenser_id;

  v_notif_id := public.fn_insert_notification(
    v_parent_owner_id,
    'workflow_forked',
    COALESCE(v_forker_display_name, v_forker_handle) || ' forked your workflow',
    NEW.title,
    '/workflows/' || NEW.id,
    jsonb_build_object(
      'forker_id',           NEW.lenser_id,
      'forker_handle',       v_forker_handle,
      'forker_display_name', v_forker_display_name,
      'parent_workflow_id',  NEW.parent_workflow_id,
      'fork_workflow_id',    NEW.id,
      'fork_title',          NEW.title
    ),
    NEW.lenser_id
  );

  IF v_notif_id IS NOT NULL THEN
    UPDATE public.notification_aggregates
       SET notification_id = v_notif_id
     WHERE recipient_id      = v_parent_owner_id
       AND notification_type = 'workflow_forked'
       AND entity_id         = NEW.parent_workflow_id
       AND notification_id   IS NULL
       AND window_end        > now();
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_workflow_forked failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_trg_notify_workflow_forked"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_trg_notify_workflow_published"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_author_display_name TEXT;
  v_author_handle       TEXT;
  v_follower            RECORD;
BEGIN
  SELECT display_name, handle
    INTO v_author_display_name, v_author_handle
    FROM lensers.profiles
   WHERE id = NEW.lenser_id;

  FOR v_follower IN
    SELECT r.source_profile_id AS follower_id
      FROM lensers.relationships r
     WHERE r.target_profile_id = NEW.lenser_id
       AND r.status            = 'accepted'
       AND r.removed_at        IS NULL
     LIMIT 5000
  LOOP
    PERFORM public.fn_insert_notification(
      v_follower.follower_id,
      'workflow_published',
      COALESCE(v_author_display_name, v_author_handle) || ' published a new workflow',
      NEW.title,
      '/workflows/' || NEW.id,
      jsonb_build_object(
        'author_id',      NEW.lenser_id,
        'author_handle',  v_author_handle,
        'workflow_id',    NEW.id,
        'workflow_title', NEW.title
      ),
      NEW.lenser_id
    );
  END LOOP;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_trg_notify_workflow_published failed for workflow %: %', NEW.id, SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_trg_notify_workflow_published"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text" DEFAULT NULL::"text", "p_visibility" "content"."visibility_enum" DEFAULT NULL::"content"."visibility_enum", "p_title" "text" DEFAULT NULL::"text", "p_description" "text" DEFAULT NULL::"text", "p_tag_ids" "uuid"[] DEFAULT NULL::"uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
  v_owner_id  uuid;
  v_tag_id    uuid;
BEGIN
  -- Auth
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Ownership
  SELECT lenser_id INTO v_owner_id
    FROM lenses.lenses
   WHERE id = p_lens_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Lens not found: %', p_lens_id
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_owner_id <> v_caller_id THEN
    RAISE EXCEPTION 'Permission denied: you do not own this lens'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- 1. Update visibility if provided
  IF p_visibility IS NOT NULL THEN
    UPDATE lenses.lenses SET visibility = p_visibility WHERE id = p_lens_id;
  END IF;

  -- 2. Content change → create a new append-only draft version
  IF p_template_body IS NOT NULL THEN
    IF length(trim(p_template_body)) < 50 THEN
      RAISE EXCEPTION 'template_body must be at least 50 characters (got %)',
        length(trim(p_template_body))
        USING ERRCODE = 'check_violation';
    END IF;

    -- Always append a new draft row (never overwrite existing version)
    PERFORM lenses.fn_create_draft_version(
      p_lens_id,
      p_template_body,
      'Updated via lens edit'
    );
  END IF;

  -- 3. Update translation
  IF p_title IS NOT NULL OR p_description IS NOT NULL
     OR p_template_body IS NOT NULL THEN
    UPDATE content.entity_translations
       SET title       = coalesce(p_title, title),
           description = coalesce(p_description, description),
           content     = coalesce(p_template_body, content)
     WHERE entity_type = 'lens'::"content"."entity_type_enum"
       AND entity_id   = p_lens_id
       AND is_original = true;
  END IF;

  -- 4. Replace tags if provided (NULL = no change, empty array = clear all)
  IF p_tag_ids IS NOT NULL THEN
    DELETE FROM content.tag_map
     WHERE entity_type = 'lens'::"content"."entity_type_enum"
       AND entity_id   = p_lens_id;

    IF array_length(p_tag_ids, 1) > 0 THEN
      FOREACH v_tag_id IN ARRAY p_tag_ids LOOP
        INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
        VALUES ('lens'::"content"."entity_type_enum", p_lens_id, v_tag_id)
        ON CONFLICT DO NOTHING;
      END LOOP;
    END IF;
  END IF;
END;
$$;


ALTER FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text", "p_visibility" "content"."visibility_enum", "p_title" "text", "p_description" "text", "p_tag_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text", "p_visibility" "content"."visibility_enum", "p_title" "text", "p_description" "text", "p_tag_ids" "uuid"[]) IS 'GRASP Controller: atomic lens update. Updates visibility, template body (via
   fn_create_draft_version — append-only, always inserts new draft row), translation,
   and tags in one transaction. SECURITY DEFINER: ownership checked internally.';



CREATE OR REPLACE FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text" DEFAULT NULL::"text", "p_visibility" "content"."visibility_enum" DEFAULT NULL::"content"."visibility_enum", "p_title" "text" DEFAULT NULL::"text", "p_description" "text" DEFAULT NULL::"text", "p_tag_ids" "uuid"[] DEFAULT NULL::"uuid"[], "p_params" "jsonb" DEFAULT NULL::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id   uuid;
  v_owner_id    uuid;
  v_tag_id      uuid;
  v_new_version lenses.versions;
  v_param       jsonb;
  v_param_id    uuid;
  v_text_tool   uuid;
  v_uuid_body   text;
BEGIN
  -- Auth
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Ownership
  SELECT lenser_id INTO v_owner_id
  FROM   lenses.lenses
  WHERE  id = p_lens_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Lens not found: %', p_lens_id
      USING ERRCODE = 'no_data_found';
  END IF;

  IF v_owner_id <> v_caller_id THEN
    RAISE EXCEPTION 'Permission denied: you do not own this lens'
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- 1. Update visibility
  IF p_visibility IS NOT NULL THEN
    UPDATE lenses.lenses SET visibility = p_visibility WHERE id = p_lens_id;
  END IF;

  -- 2. Content change → new append-only draft version + version_parameters
  IF p_template_body IS NOT NULL THEN
    IF length(trim(p_template_body)) < 50 THEN
      RAISE EXCEPTION 'template_body must be at least 50 characters (got %)',
        length(trim(p_template_body))
        USING ERRCODE = 'check_violation';
    END IF;

    -- Create draft version (receives [[label]] body; we'll rewrite it below)
    v_new_version := lenses.fn_create_draft_version(
      p_lens_id,
      p_template_body,
      'Updated via lens edit'
    );

    -- Insert version_parameters and build [[:uuid]] body
    v_uuid_body := p_template_body;

    IF p_params IS NOT NULL AND jsonb_array_length(p_params) > 0 THEN
      SELECT id INTO v_text_tool FROM lenses.tools WHERE key = 'text' LIMIT 1;

      FOR v_param IN SELECT * FROM jsonb_array_elements(p_params) LOOP
        INSERT INTO lenses.version_parameters (version_id, label, tool_id)
        VALUES (
          v_new_version.id,
          v_param->>'label',
          COALESCE(
            NULLIF(v_param->>'tool_id', '')::uuid,
            v_text_tool
          )
        )
        RETURNING id INTO v_param_id;

        v_uuid_body := replace(
          v_uuid_body,
          '[[' || (v_param->>'label') || ']]',
          '[[:'|| v_param_id::text ||']]'
        );
      END LOOP;

      -- Store the rewritten ([[:uuid]]) body in the version row
      UPDATE lenses.versions
      SET template_body = v_uuid_body
      WHERE id = v_new_version.id;
    END IF;
  END IF;

  -- 3. Update translation — keeps human-readable [[label]] format
  IF p_title IS NOT NULL OR p_description IS NOT NULL OR p_template_body IS NOT NULL THEN
    UPDATE content.entity_translations
    SET
      title       = COALESCE(p_title,        title),
      description = COALESCE(p_description,  description),
      content     = COALESCE(p_template_body, content)  -- [[label]] format
    WHERE entity_type = 'lens'::content.entity_type_enum
      AND entity_id   = p_lens_id
      AND is_original = true;
  END IF;

  -- 4. Replace tags
  IF p_tag_ids IS NOT NULL THEN
    DELETE FROM content.tag_map
    WHERE entity_type = 'lens'::content.entity_type_enum
      AND entity_id   = p_lens_id;

    IF array_length(p_tag_ids, 1) > 0 THEN
      FOREACH v_tag_id IN ARRAY p_tag_ids LOOP
        INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
        VALUES ('lens'::content.entity_type_enum, p_lens_id, v_tag_id)
        ON CONFLICT DO NOTHING;
      END LOOP;
    END IF;
  END IF;
END;
$$;


ALTER FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text", "p_visibility" "content"."visibility_enum", "p_title" "text", "p_description" "text", "p_tag_ids" "uuid"[], "p_params" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_upsert_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text" DEFAULT NULL::"text", "p_parent_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "lenses"."versions"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'public'
    AS $$
DECLARE
  v_draft_id            uuid;
  v_next_number         int;
  v_last_published_body text;
  v_result              lenses.versions;
BEGIN
  -- Validate minimum length
  IF length(trim(p_template_body)) < 50 THEN
    RAISE EXCEPTION 'template_body must be at least 50 characters (got %)',
      length(trim(p_template_body))
      USING ERRCODE = 'check_violation';
  END IF;

  -- Content-change detection: fetch last published version's template_body
  -- Uses idx_lens_versions_published_latest
  SELECT v.template_body
  INTO v_last_published_body
  FROM lenses.versions v
  WHERE v.lens_id = p_lens_id
    AND v.status = 'published'::"content"."content_status"
  ORDER BY v.version_number DESC
  LIMIT 1;

  -- If content is identical to last published version → no-op
  IF v_last_published_body IS NOT NULL
     AND v_last_published_body = p_template_body THEN

    -- Return existing draft if one exists
    SELECT *
    INTO v_result
    FROM lenses.versions
    WHERE lens_id = p_lens_id
      AND status = 'draft'::"content"."content_status"
    ORDER BY version_number DESC
    LIMIT 1;

    IF v_result.id IS NOT NULL THEN
      RETURN v_result;
    END IF;

    -- No draft exists — return last published version
    SELECT *
    INTO v_result
    FROM lenses.versions
    WHERE lens_id = p_lens_id
      AND status = 'published'::"content"."content_status"
    ORDER BY version_number DESC
    LIMIT 1;

    RETURN v_result;
  END IF;

  -- Look for the current draft version (uses idx_lens_versions_draft)
  SELECT id
  INTO v_draft_id
  FROM lenses.versions
  WHERE lens_id = p_lens_id
    AND status = 'draft'::"content"."content_status"
  ORDER BY version_number DESC
  LIMIT 1;

  IF v_draft_id IS NOT NULL THEN
    -- Reuse: update the existing draft in place
    UPDATE lenses.versions
    SET
      template_body = p_template_body,
      changelog     = COALESCE(p_changelog, changelog)
    WHERE id = v_draft_id
    RETURNING * INTO v_result;
  ELSE
    -- All prior versions are published/archived — open a new draft
    SELECT COALESCE(MAX(version_number), 0) + 1
    INTO v_next_number
    FROM lenses.versions
    WHERE lens_id = p_lens_id;

    INSERT INTO lenses.versions (
      lens_id,
      version_number,
      template_body,
      status,
      changelog,
      parent_version_id
    )
    VALUES (
      p_lens_id,
      v_next_number,
      p_template_body,
      'draft'::"content"."content_status",
      COALESCE(p_changelog, 'New version'),
      p_parent_version_id
    )
    RETURNING * INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "lenses"."fn_upsert_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_upsert_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") IS 'GRASP Creator: creates or updates draft lens versions with content-change detection. Skips draft creation/update when new content is identical to the last published version. Validates 50-char minimum on template_body. SECURITY DEFINER: bypasses RLS — caller security enforced by calling function.';



CREATE OR REPLACE FUNCTION "lenses"."fn_upsert_workflow_schedule_internal"("p_workflow_id" "uuid", "p_schedule_id" "uuid" DEFAULT NULL::"uuid", "p_cron_expr" "text" DEFAULT '* * * * *'::"text", "p_timezone" "text" DEFAULT 'UTC'::"text", "p_description" "text" DEFAULT NULL::"text", "p_approval_policy" "jsonb" DEFAULT '{"requiresApproval": true}'::"jsonb", "p_is_active" boolean DEFAULT true, "p_global_model_id" "text" DEFAULT NULL::"text", "p_assignee_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_assignment_id" "uuid" DEFAULT NULL::"uuid", "p_retry_policy" "jsonb" DEFAULT '{"maxRetries": 1}'::"jsonb", "p_failure_policy" "jsonb" DEFAULT '{"mode": "isolate"}'::"jsonb", "p_queue_policy" "jsonb" DEFAULT '{"mode": "parallel"}'::"jsonb", "p_inputs_template" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "cron_expr" "text", "timezone" "text", "description" "text", "is_active" boolean, "assignee_type" "text", "assignee_id" "uuid", "workflow_assignment_id" "uuid", "approval_policy" "jsonb", "retry_policy" "jsonb", "failure_policy" "jsonb", "queue_policy" "jsonb", "inputs_template" "jsonb", "global_model_id" "text", "next_run_at" timestamp with time zone, "last_run_at" timestamp with time zone, "last_run_id" "uuid", "last_dispatch_status" "text", "last_error_at" timestamp with time zone, "last_error_message" "text", "last_completed_at" timestamp with time zone, "last_result" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_schedule_id uuid;
BEGIN
  IF p_schedule_id IS NULL THEN
    INSERT INTO lenses.workflow_schedules (
      workflow_id,
      cron_expr,
      timezone,
      description,
      global_model_id,
      inputs_template,
      is_active,
      assignee_id,
      workflow_assignment_id,
      approval_policy,
      retry_policy,
      failure_policy,
      queue_policy
    ) VALUES (
      p_workflow_id,
      p_cron_expr,
      COALESCE(NULLIF(trim(p_timezone), ''), 'UTC'),
      p_description,
      p_global_model_id,
      COALESCE(p_inputs_template, '{}'::jsonb),
      p_is_active,
      p_assignee_id,
      p_workflow_assignment_id,
      COALESCE(p_approval_policy, '{"requiresApproval":true}'::jsonb),
      COALESCE(p_retry_policy, '{"maxRetries":1}'::jsonb),
      COALESCE(p_failure_policy, '{"mode":"isolate"}'::jsonb),
      COALESCE(p_queue_policy, '{"mode":"parallel"}'::jsonb)
    )
    RETURNING workflow_schedules.id INTO v_schedule_id;
  ELSE
    UPDATE lenses.workflow_schedules s
    SET
      cron_expr              = p_cron_expr,
      timezone               = COALESCE(NULLIF(trim(p_timezone), ''), s.timezone, 'UTC'),
      description            = COALESCE(p_description, s.description),
      global_model_id        = p_global_model_id,
      inputs_template        = COALESCE(p_inputs_template, '{}'::jsonb),
      is_active              = p_is_active,
      assignee_id            = p_assignee_id,
      workflow_assignment_id = p_workflow_assignment_id,
      approval_policy        = COALESCE(p_approval_policy, s.approval_policy, '{}'::jsonb),
      retry_policy           = COALESCE(p_retry_policy, s.retry_policy, '{}'::jsonb),
      failure_policy         = COALESCE(p_failure_policy, s.failure_policy, '{}'::jsonb),
      queue_policy           = COALESCE(p_queue_policy, s.queue_policy, '{}'::jsonb),
      last_dispatch_status   = CASE
        WHEN p_is_active THEN NULL
        ELSE COALESCE(s.last_dispatch_status, 'paused')
      END,
      last_error_at          = CASE
        WHEN p_is_active THEN NULL
        ELSE s.last_error_at
      END,
      last_error_message     = CASE
        WHEN p_is_active THEN NULL
        ELSE s.last_error_message
      END
    WHERE s.id = p_schedule_id
      AND s.workflow_id = p_workflow_id
    RETURNING s.id INTO v_schedule_id;

    IF v_schedule_id IS NULL THEN
      RAISE EXCEPTION 'Schedule not found or not owned by the active workspace'
        USING ERRCODE = '42501';
    END IF;
  END IF;

  RETURN QUERY
  SELECT
    s.id,
    s.workflow_id,
    s.cron_expr,
    s.timezone,
    s.description,
    s.is_active,
    s.assignee_type,
    s.assignee_id,
    s.workflow_assignment_id,
    s.approval_policy,
    s.retry_policy,
    s.failure_policy,
    s.queue_policy,
    s.inputs_template,
    s.global_model_id,
    s.next_run_at,
    s.last_run_at,
    s.last_run_id,
    s.last_dispatch_status,
    s.last_error_at,
    s.last_error_message,
    s.last_completed_at,
    s.last_result,
    s.created_at
  FROM lenses.workflow_schedules s
  WHERE s.id = v_schedule_id;
END;
$$;


ALTER FUNCTION "lenses"."fn_upsert_workflow_schedule_internal"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_validate_inputs"("p_version_id" "uuid", "p_inputs" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'media', 'lensers', 'public'
    AS $_$
DECLARE
  vp          RECORD;
  v_raw       text;
  v_schema    jsonb;
  v_min       numeric;
  v_max       numeric;
  v_num       numeric;
  v_caller_id uuid;
BEGIN
  -- No version → nothing to validate (allow ad-hoc / legacy requests)
  IF p_version_id IS NULL THEN
    RETURN;
  END IF;

  v_caller_id := "lensers"."get_auth_lenser_id"();

  -- Join tools to get type, required, options, validation_schema.
  -- Use parameter label as the input key (consistent with fn_create_lens).
  FOR vp IN
    SELECT
      vp_row."label"              AS "key",
      t."type"                    AS "type",
      t."required"                AS "required",
      t."options"                 AS "options",
      t."validation_schema"       AS "validation_schema"
    FROM   "lenses"."version_parameters" vp_row
    JOIN   "lenses"."tools"              t ON t."id" = vp_row."tool_id"
    WHERE  vp_row."version_id" = p_version_id
    ORDER  BY t."sort_order", vp_row."label"
    LIMIT  200
  LOOP
    v_raw    := "p_inputs"->>vp."key";
    v_schema := vp."validation_schema";

    -- 1. Required field must be present and non-empty
    IF vp."required" AND (v_raw IS NULL OR trim(v_raw) = '') THEN
      RAISE EXCEPTION 'Required parameter "%" is missing or empty', vp."key"
        USING ERRCODE = 'check_violation';
    END IF;

    -- Skip type checks for absent optional parameters
    CONTINUE WHEN v_raw IS NULL OR trim(v_raw) = '';

    -- 2. Type-compatibility check + schema enforcement
    BEGIN
      CASE vp."type"

        -- ── Legacy numeric ───────────────────────────────────────────────
        WHEN 'number' THEN
          v_num := v_raw::"numeric";
          IF v_schema IS NOT NULL THEN
            IF v_schema->>'min' IS NOT NULL THEN
              v_min := (v_schema->>'min')::"numeric";
              IF v_num < v_min THEN
                RAISE EXCEPTION 'Parameter "%" value % is below minimum %',
                  vp."key", v_num, v_min USING ERRCODE = 'check_violation';
              END IF;
            END IF;
            IF v_schema->>'max' IS NOT NULL THEN
              v_max := (v_schema->>'max')::"numeric";
              IF v_num > v_max THEN
                RAISE EXCEPTION 'Parameter "%" value % exceeds maximum %',
                  vp."key", v_num, v_max USING ERRCODE = 'check_violation';
              END IF;
            END IF;
          END IF;

        -- ── Integer ──────────────────────────────────────────────────────
        WHEN 'integer' THEN
          v_num := v_raw::"bigint";
          IF v_schema IS NOT NULL THEN
            IF v_schema->>'min' IS NOT NULL THEN
              v_min := (v_schema->>'min')::"numeric";
              IF v_num < v_min THEN
                RAISE EXCEPTION 'Parameter "%" value % is below minimum %',
                  vp."key", v_num, v_min USING ERRCODE = 'check_violation';
              END IF;
            END IF;
            IF v_schema->>'max' IS NOT NULL THEN
              v_max := (v_schema->>'max')::"numeric";
              IF v_num > v_max THEN
                RAISE EXCEPTION 'Parameter "%" value % exceeds maximum %',
                  vp."key", v_num, v_max USING ERRCODE = 'check_violation';
              END IF;
            END IF;
          END IF;

        -- ── Float ────────────────────────────────────────────────────────
        WHEN 'float' THEN
          v_num := v_raw::"double precision";
          IF v_schema IS NOT NULL THEN
            IF v_schema->>'min' IS NOT NULL THEN
              v_min := (v_schema->>'min')::"numeric";
              IF v_num < v_min THEN
                RAISE EXCEPTION 'Parameter "%" value % is below minimum %',
                  vp."key", v_num, v_min USING ERRCODE = 'check_violation';
              END IF;
            END IF;
            IF v_schema->>'max' IS NOT NULL THEN
              v_max := (v_schema->>'max')::"numeric";
              IF v_num > v_max THEN
                RAISE EXCEPTION 'Parameter "%" value % exceeds maximum %',
                  vp."key", v_num, v_max USING ERRCODE = 'check_violation';
              END IF;
            END IF;
          END IF;

        -- ── Decimal / Numeric ─────────────────────────────────────────────
        WHEN 'decimal' THEN
          v_num := v_raw::"numeric";
          IF v_schema IS NOT NULL THEN
            IF v_schema->>'min' IS NOT NULL THEN
              v_min := (v_schema->>'min')::"numeric";
              IF v_num < v_min THEN
                RAISE EXCEPTION 'Parameter "%" value % is below minimum %',
                  vp."key", v_num, v_min USING ERRCODE = 'check_violation';
              END IF;
            END IF;
            IF v_schema->>'max' IS NOT NULL THEN
              v_max := (v_schema->>'max')::"numeric";
              IF v_num > v_max THEN
                RAISE EXCEPTION 'Parameter "%" value % exceeds maximum %',
                  vp."key", v_num, v_max USING ERRCODE = 'check_violation';
              END IF;
            END IF;
          END IF;

        -- ── Boolean ───────────────────────────────────────────────────────
        WHEN 'boolean' THEN
          IF lower(v_raw) NOT IN ('true', 'false', '1', '0') THEN
            RAISE EXCEPTION
              'Parameter "%" must be a boolean value (true/false), got: %',
              vp."key", v_raw
              USING ERRCODE = 'check_violation';
          END IF;

        -- ── JSON ──────────────────────────────────────────────────────────
        WHEN 'json' THEN
          PERFORM v_raw::"jsonb";

        -- ── Select (enum membership) ──────────────────────────────────────
        WHEN 'select' THEN
          IF vp."options" IS NOT NULL
             AND jsonb_typeof(vp."options") = 'array'
             AND NOT EXISTS (
               SELECT 1
               FROM   jsonb_array_elements(vp."options") opt
               WHERE  opt->>'value' = v_raw
             ) THEN
            RAISE EXCEPTION
              'Parameter "%" value "%" is not in the allowed options list',
              vp."key", v_raw
              USING ERRCODE = 'check_violation';
          END IF;

        -- ── URL ───────────────────────────────────────────────────────────
        WHEN 'url' THEN
          -- Must start with http:// or https:// (no scheme injection)
          IF v_raw !~ '^https?://' THEN
            RAISE EXCEPTION
              'Parameter "%" must be a valid URL starting with http:// or https://, got: %',
              vp."key", left(v_raw, 100)
              USING ERRCODE = 'check_violation';
          END IF;
          -- Optional: allowedSchemes from validation_schema
          IF v_schema IS NOT NULL AND v_schema->'urlScheme' IS NOT NULL
             AND jsonb_typeof(v_schema->'urlScheme') = 'array'
             AND NOT EXISTS (
               SELECT 1
               FROM   jsonb_array_elements_text(v_schema->'urlScheme') scheme
               WHERE  lower(v_raw) LIKE lower(scheme) || '://%'
             ) THEN
            RAISE EXCEPTION
              'Parameter "%" URL scheme is not allowed',
              vp."key"
              USING ERRCODE = 'check_violation';
          END IF;

        -- ── Date ──────────────────────────────────────────────────────────
        WHEN 'date' THEN
          PERFORM v_raw::"date";

        -- ── Datetime ──────────────────────────────────────────────────────
        WHEN 'datetime' THEN
          PERFORM v_raw::"timestamptz";

        -- ── File (attachment) ─────────────────────────────────────────────
        WHEN 'file' THEN
          -- Value must be a valid UUID
          IF v_raw !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN
            RAISE EXCEPTION
              'Parameter "%" must be a media object UUID, got invalid value',
              vp."key"
              USING ERRCODE = 'check_violation';
          END IF;
          -- Referenced media.objects row must exist and be owned by the caller
          IF v_caller_id IS NOT NULL THEN
            IF NOT EXISTS (
              SELECT 1
              FROM   "media"."objects" mo
              WHERE  mo."id"               = v_raw::"uuid"
                AND  mo."owner_lenser_id"  = v_caller_id
                AND  mo."lifecycle_state" IN ('pending', 'active')
            ) THEN
              RAISE EXCEPTION
                'Parameter "%" references a media object that does not exist or is not accessible',
                vp."key"
                USING ERRCODE = 'check_violation';
            END IF;
          END IF;

        ELSE
          NULL;  -- 'text', 'textarea': no structural enforcement
      END CASE;

    EXCEPTION
      WHEN invalid_text_representation
        OR numeric_value_out_of_range
        OR invalid_parameter_value
        OR invalid_datetime_format
      THEN
        RAISE EXCEPTION
          'Parameter "%" has an invalid value for type "%": %',
          vp."key", vp."type", left(v_raw, 200)
          USING ERRCODE = 'check_violation';
    END;
  END LOOP;
END;
$_$;


ALTER FUNCTION "lenses"."fn_validate_inputs"("p_version_id" "uuid", "p_inputs" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_validate_inputs"("p_version_id" "uuid", "p_inputs" "jsonb") IS 'GRASP Information Expert: validates execution inputs against the declared version_parameters for a given lens version. Joins lenses.tools via tool_id to resolve type, required, options, and validation_schema. Uses parameter label as the input key. Supports types: text, textarea, number, integer, float, decimal, boolean, json, select, url (regex + scheme allowlist), date, datetime, file (UUID + caller-owned media.objects row). Called by execution.fn_run_lens. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "lenses"."fn_workflow_has_cycle"("p_workflow_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
  WITH RECURSIVE walk AS (
    SELECT
      e.source_node_id,
      e.target_node_id,
      ARRAY[e.source_node_id, e.target_node_id]::uuid[] AS path,
      e.source_node_id = e.target_node_id AS cycle
    FROM lenses.workflow_edges e
    WHERE e.workflow_id = p_workflow_id

    UNION ALL

    SELECT
      w.source_node_id,
      e.target_node_id,
      w.path || e.target_node_id,
      e.target_node_id = ANY(w.path) AS cycle
    FROM walk w
    JOIN lenses.workflow_edges e
      ON e.workflow_id = p_workflow_id
     AND e.source_node_id = w.target_node_id
    WHERE NOT w.cycle
      AND cardinality(w.path) < 256
  )
  SELECT EXISTS (SELECT 1 FROM walk WHERE cycle);
$$;


ALTER FUNCTION "lenses"."fn_workflow_has_cycle"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."fn_workflow_has_cycle"("p_workflow_id" "uuid") IS 'Returns true when the workflow graph contains a cycle. Used to block schedule activation for invalid DAGs.';



CREATE OR REPLACE FUNCTION "lenses"."fn_workflow_snapshot_hash"("p_version_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'extensions', 'public'
    AS $$
DECLARE
  v_payload JSONB;
BEGIN
  SELECT jsonb_build_object(
    'nodes', COALESCE((
      SELECT jsonb_agg(
        jsonb_build_object(
          'id', n.id,
          'source_node_id', n.source_node_id,
          'lens_id', n.lens_id,
          'version_id', n.version_id,
          'position_x', n.position_x,
          'position_y', n.position_y,
          'label', n.label,
          'ordinal', n.ordinal,
          'config', n.config
        )
        ORDER BY n.ordinal, n.id
      )
      FROM lenses.workflow_version_nodes n
      WHERE n.workflow_version_id = p_version_id
    ), '[]'::jsonb),
    'edges', COALESCE((
      SELECT jsonb_agg(
        jsonb_build_object(
          'source_node_id', e.source_node_id,
          'target_node_id', e.target_node_id,
          'source_output_key', e.source_output_key,
          'target_param_label', e.target_param_label
        )
        ORDER BY e.source_node_id, e.target_node_id, e.target_param_label
      )
      FROM lenses.workflow_version_edges e
      WHERE e.workflow_version_id = p_version_id
    ), '[]'::jsonb)
  )
  INTO v_payload;

  RETURN encode(extensions.digest(v_payload::text, 'sha256'), 'hex');
END;
$$;


ALTER FUNCTION "lenses"."fn_workflow_snapshot_hash"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_xp_on_tutorial_completed"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'xp', 'execution', 'public', 'extensions'
    AS $$
DECLARE
  v_rule_key  TEXT;
  v_forum_app CONSTANT UUID := '00000000-0000-0000-0000-000000000001';
BEGIN
  v_rule_key := CASE NEW.kind
    WHEN 'tutorial'     THEN 'TUTORIAL_COMPLETED'
    WHEN 'walkthrough'  THEN 'WALKTHROUGH_COMPLETED'
    ELSE NULL
  END;

  IF v_rule_key IS NULL THEN
    RETURN NEW;
  END IF;

  PERFORM execution.fn_xp_apply_safe(
    NEW.lenser_id,
    v_rule_key,
    'system'::xp.source_enum,
    'tutorial_completion',
    NEW.id,
    v_forum_app
  );
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE NOTICE 'fn_xp_on_tutorial_completed: % (completion=%)', SQLERRM, NEW.id;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_xp_on_tutorial_completed"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."fn_xp_on_workflow_run_received"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'xp', 'execution', 'public', 'extensions'
    AS $$
DECLARE
  v_owner_lenser_id UUID;
  v_forum_app       CONSTANT UUID := '00000000-0000-0000-0000-000000000001';
BEGIN
  -- Only award when a real user triggers a run (not schedule/subflow/system)
  IF NEW.triggered_by IS NULL THEN
    RETURN NEW;
  END IF;

  -- Look up the workflow owner and check visibility in one query
  SELECT w.lenser_id INTO v_owner_lenser_id
  FROM lenses.workflows w
  WHERE w.id = NEW.workflow_id
    AND w.visibility = 'public'
    AND w.lenser_id IS DISTINCT FROM NEW.triggered_by;

  IF v_owner_lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  PERFORM execution.fn_xp_apply_safe(
    v_owner_lenser_id,
    'WORKFLOW_RUN_RECEIVED',
    'system'::xp.source_enum,
    'workflow_run',
    NEW.id,
    v_forum_app
  );
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE NOTICE 'fn_xp_on_workflow_run_received: % (run=%)', SQLERRM, NEW.id;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."fn_xp_on_workflow_run_received"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."set_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."trg_accumulate_node_cost_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$
DECLARE
  v_run_id uuid;
BEGIN
  -- Only act when status transitions to 'completed'
  IF NEW.status = 'completed' AND (OLD.status IS NULL OR OLD.status <> 'completed') THEN
    v_run_id := NEW.run_id;

    -- Atomically increment spent_credits and update cost_metadata
    UPDATE lenses.workflow_runs
    SET
      spent_credits = spent_credits + NEW.cost_credits,
      cost_metadata = cost_metadata || jsonb_build_object(
        NEW.node_id::text,
        jsonb_build_object(
          'input_tokens', NEW.input_tokens,
          'output_tokens', NEW.output_tokens,
          'cost_credits', NEW.cost_credits
        )
      )
    WHERE id = v_run_id;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."trg_accumulate_node_cost_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."trg_block_parameters_insert"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses'
    AS $$
BEGIN
  RAISE EXCEPTION
    'lenses.parameters is deprecated and no longer accepts new rows. '
    'Use lenses.version_parameters instead. '
    'See migration 20260323120002.';
END;
$$;


ALTER FUNCTION "lenses"."trg_block_parameters_insert"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."trg_cleanup_lens_content_refs"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
  PERFORM "content"."fn_cleanup_entity_refs"('lens'::"content"."entity_type_enum", OLD."id");
  RETURN OLD;
END;
$$;


ALTER FUNCTION "lenses"."trg_cleanup_lens_content_refs"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."trg_notify_cron_run_change"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_owner_id       UUID;
  v_wf_name        TEXT;
  v_notif_type     TEXT;
  v_is_scheduled   BOOL;
  v_ai_owner_id    UUID;
  v_ai_profile_id  UUID;
BEGIN
  IF OLD.status IS NOT DISTINCT FROM NEW.status THEN RETURN NEW; END IF;
  IF NEW.status NOT IN ('completed', 'failed') THEN RETURN NEW; END IF;

  -- Only fire for scheduled workflows
  SELECT EXISTS (
    SELECT 1 FROM lenses.workflow_schedules
    WHERE  workflow_id = NEW.workflow_id AND is_active = true
  ) INTO v_is_scheduled;

  IF NOT v_is_scheduled THEN RETURN NEW; END IF;

  SELECT lenser_id, title INTO v_owner_id, v_wf_name
  FROM   lenses.workflows WHERE id = NEW.workflow_id;

  v_notif_type := CASE NEW.status WHEN 'completed' THEN 'cron_run_completed' ELSE 'cron_run_failed' END;

  -- Notify workflow owner (could be human or AI lenser profile)
  IF v_owner_id IS NOT NULL THEN
    PERFORM public.fn_insert_notification(
      v_owner_id,
      v_notif_type,
      'Scheduled run ' || NEW.status || COALESCE(': ' || v_wf_name, ''),
      NULL,
      '/workflows/' || NEW.workflow_id,
      jsonb_build_object(
        'workflow_run_id', NEW.id,
        'workflow_id',     NEW.workflow_id,
        'workflow_name',   v_wf_name,
        'status',          NEW.status
      )
    );

    -- If the workflow owner is an AI lenser, also notify the human owner
    SELECT al.id, al.profile_id INTO v_ai_profile_id, v_ai_profile_id
    FROM   agents.ai_lensers al WHERE al.profile_id = v_owner_id;

    IF FOUND THEN
      -- v_owner_id is an AI lenser's profile — find their human owner
      SELECT o.owner_lenser_id INTO v_ai_owner_id
      FROM   agents.ownerships o
      JOIN   agents.ai_lensers al ON al.id = o.ai_lenser_id
      WHERE  al.profile_id = v_owner_id
      AND    o.role = 'owner'
      AND    o.revoked_at IS NULL
      LIMIT  1;

      IF v_ai_owner_id IS NOT NULL THEN
        PERFORM public.fn_insert_notification(
          v_ai_owner_id,
          CASE NEW.status WHEN 'completed' THEN 'agent_cron_result' ELSE 'agent_critical' END,
          'Agent CRON run ' || NEW.status || COALESCE(': ' || v_wf_name, ''),
          NULL,
          '/workflows/' || NEW.workflow_id,
          jsonb_build_object(
            'ai_profile_id',   v_owner_id,
            'workflow_run_id', NEW.id,
            'workflow_name',   v_wf_name,
            'status',          NEW.status
          )
        );
      END IF;
    END IF;
  END IF;

  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'trg_notify_cron_run_change failed: %', SQLERRM;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."trg_notify_cron_run_change"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."trg_steps_sync_lens_id"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
BEGIN
  -- If version_id is set and lens_id is NULL, derive lens_id from the version row
  IF NEW."version_id" IS NOT NULL AND NEW."lens_id" IS NULL THEN
    SELECT "lens_id"
    INTO   NEW."lens_id"
    FROM   "lenses"."versions"
    WHERE  "id" = NEW."version_id";
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."trg_steps_sync_lens_id"() OWNER TO "postgres";


COMMENT ON FUNCTION "lenses"."trg_steps_sync_lens_id"() IS 'GRASP Low Coupling: auto-populates the deprecated lens_id column from version_id to prevent NULL violations in legacy code paths. Once lens_id is dropped, this trigger can be dropped too.';



CREATE OR REPLACE FUNCTION "lenses"."trg_workflow_edges_acyclic_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses'
    AS $$
BEGIN
  IF NOT lenses.fn_check_dag_acyclic_kahn(NEW.workflow_id) THEN
    RAISE EXCEPTION 'Cycle detected in workflow DAG'
      USING DETAIL = 'Adding this edge would create a cycle in the workflow graph.',
            ERRCODE = 'check_violation';
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."trg_workflow_edges_acyclic_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "lenses"."trg_workflow_version_immutable_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'lenses', 'pg_catalog'
    AS $$
BEGIN
  IF OLD.status = 'published' AND NEW.status = 'published' THEN
    RAISE EXCEPTION 'Cannot modify a published workflow version'
      USING ERRCODE = 'check_violation';
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "lenses"."trg_workflow_version_immutable_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "media"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'media', 'public'
    AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$;


ALTER FUNCTION "media"."set_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "media"."trg_fn_audit_visibility"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
BEGIN
  IF OLD.visibility IS DISTINCT FROM NEW.visibility THEN
    INSERT INTO audit.events (event_type, actor_type, severity, payload)
    VALUES (
      'media.visibility_changed',
      'user',
      'info',
      jsonb_build_object(
        'object_id',      NEW.id,
        'old_visibility', OLD.visibility,
        'new_visibility', NEW.visibility,
        'changed_at',     now()
      )
    );
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "media"."trg_fn_audit_visibility"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "organizations"."fn_create_org_workspace"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'organizations', 'tenancy', 'lensers', 'public'
    AS $$
DECLARE
    v_workspace_id uuid;
    v_slug         text;
    v_suffix       int := 0;
BEGIN
    -- Build a unique workspace slug
    v_slug := NEW.slug || '-org';
    WHILE EXISTS (SELECT 1 FROM tenancy.workspaces WHERE slug = v_slug) LOOP
        v_suffix := v_suffix + 1;
        v_slug   := NEW.slug || '-org-' || v_suffix;
    END LOOP;

    -- org row is now committed (AFTER trigger), org_id FK is safe
    INSERT INTO tenancy.workspaces (slug, type, display_name, org_id)
    VALUES (v_slug, 'organization', NEW.display_name, NEW.id)
    RETURNING id INTO v_workspace_id;

    -- Stamp workspace_id back onto the org row
    UPDATE organizations.organizations
    SET    workspace_id = v_workspace_id
    WHERE  id = NEW.id;

    RETURN NULL; -- AFTER trigger return value is ignored
END;
$$;


ALTER FUNCTION "organizations"."fn_create_org_workspace"() OWNER TO "postgres";


COMMENT ON FUNCTION "organizations"."fn_create_org_workspace"() IS 'AFTER INSERT trigger on organizations.organizations. Automatically creates an organization-type workspace in tenancy.workspaces and stamps workspace_id on the new org row. Must be AFTER INSERT so the org primary key exists before the workspaces_org_fkey FK is checked.';



CREATE TABLE IF NOT EXISTS "organizations"."organizations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "status" "organizations"."status_enum" DEFAULT 'active'::"organizations"."status_enum" NOT NULL,
    "org_type" "organizations"."org_type_enum" DEFAULT 'commercial'::"organizations"."org_type_enum" NOT NULL,
    "created_by" "uuid" NOT NULL,
    "slug" character varying(64) NOT NULL,
    "display_name" character varying(180) NOT NULL,
    "legal_name" character varying(180),
    "website" "text",
    "industry" character varying(120),
    "billing_email" "text",
    "default_timezone" character varying(120) DEFAULT 'UTC'::character varying,
    "avatar_url" "text",
    "kill_switch_reason" "text",
    "kill_switched_at" timestamp with time zone,
    "kill_switched_by" "uuid",
    "trial_expires_at" timestamp with time zone,
    "trial_used" boolean DEFAULT false NOT NULL,
    "trial_used_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "workspace_id" "uuid"
);


ALTER TABLE "organizations"."organizations" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."organizations" IS 'Top-level commercial / nonprofit / educational entity. One row per organization. Slug is globally unique.';



COMMENT ON COLUMN "organizations"."organizations"."status" IS 'Lifecycle state: active, inactive, suspended, closed. suspended blocks all operations; closed is terminal.';



COMMENT ON COLUMN "organizations"."organizations"."slug" IS 'URL-safe unique identifier. Used in routes like /org/<slug>. Format: 4-64 chars, lowercase alphanumeric with hyphens/underscores.';



COMMENT ON COLUMN "organizations"."organizations"."kill_switch_reason" IS 'Admin-provided reason when an organization is force-suspended. NULL when not kill-switched.';



COMMENT ON COLUMN "organizations"."organizations"."trial_expires_at" IS 'When the organization trial period ends. NULL if no trial or trial already used.';



COMMENT ON COLUMN "organizations"."organizations"."workspace_id" IS 'The organization-type workspace that scopes all resources (keys, executions, media) belonging to this org. Created automatically on org creation.';



CREATE OR REPLACE FUNCTION "organizations"."fn_create_organization"("p_display_name" character varying, "p_slug" character varying, "p_org_type" "text" DEFAULT 'commercial'::"text", "p_legal_name" character varying DEFAULT NULL::character varying, "p_website" "text" DEFAULT NULL::"text", "p_industry" character varying DEFAULT NULL::character varying, "p_billing_email" "text" DEFAULT NULL::"text") RETURNS "organizations"."organizations"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'organizations', 'lensers'
    AS $_$
DECLARE
    v_lenser_id uuid;
    v_org organizations.organizations;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    -- Validate org_type
    IF p_org_type NOT IN ('commercial', 'nonprofit', 'educational') THEN
        RAISE EXCEPTION 'Invalid org_type: %. Must be commercial, nonprofit, or educational.', p_org_type;
    END IF;

    -- Validate slug format
    IF p_slug !~ '^[a-z0-9][a-z0-9_-]{2,62}[a-z0-9]$' THEN
        RAISE EXCEPTION 'Invalid slug format. Must be 4-64 chars, lowercase alphanumeric with hyphens/underscores.';
    END IF;

    -- Create organization
    INSERT INTO organizations.organizations (
        display_name, slug, org_type, legal_name,
        website, industry, billing_email, created_by
    )
    VALUES (
        p_display_name, p_slug, p_org_type::organizations.org_type_enum, p_legal_name,
        p_website, p_industry, p_billing_email, v_lenser_id
    )
    RETURNING * INTO v_org;

    -- Auto-insert creator as owner
    INSERT INTO organizations.members (org_id, lenser_id, role)
    VALUES (v_org.id, v_lenser_id, 'owner');

    -- Log creation
    INSERT INTO organizations.audit_logs (org_id, lenser_id, action, metadata)
    VALUES (v_org.id, v_lenser_id, 'created', jsonb_build_object(
        'display_name', p_display_name,
        'slug', p_slug,
        'org_type', p_org_type
    ));

    RETURN v_org;
END;
$_$;


ALTER FUNCTION "organizations"."fn_create_organization"("p_display_name" character varying, "p_slug" character varying, "p_org_type" "text", "p_legal_name" character varying, "p_website" "text", "p_industry" character varying, "p_billing_email" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "organizations"."fn_create_organization"("p_display_name" character varying, "p_slug" character varying, "p_org_type" "text", "p_legal_name" character varying, "p_website" "text", "p_industry" character varying, "p_billing_email" "text") IS 'SECURITY DEFINER helper to create an organization. Validates slug format and org_type, creates the org record, auto-inserts the caller as owner, and logs the creation event. Requires authentication via lensers.get_auth_lenser_id().';



CREATE TABLE IF NOT EXISTS "organizations"."members" (
    "org_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "role" "organizations"."member_role_enum" DEFAULT 'member'::"organizations"."member_role_enum" NOT NULL,
    "invited_by" "uuid",
    "joined_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "organizations"."members" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."members" IS 'Organization membership. Composite PK (org_id, lenser_id). Exactly one owner per org enforced by partial unique index.';



COMMENT ON COLUMN "organizations"."members"."role" IS 'Member role within the org. owner has full control; admin can manage members; manager, billing, member, viewer have decreasing permissions. Exactly one owner per org enforced by partial unique index.';



COMMENT ON COLUMN "organizations"."members"."invited_by" IS 'Profile that invited this member. NULL for the founding owner. ON DELETE SET NULL preserves membership even if inviter is deleted.';



CREATE OR REPLACE FUNCTION "organizations"."fn_invite_member"("p_org_id" "uuid", "p_lenser_id" "uuid", "p_role" "text" DEFAULT 'member'::"text") RETURNS "organizations"."members"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'organizations', 'lensers'
    AS $$
DECLARE
    v_caller_id uuid;
    v_caller_role text;
    v_member organizations.members;
BEGIN
    v_caller_id := lensers.get_auth_lenser_id();
    IF v_caller_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    -- Check caller is owner or admin
    SELECT role::text INTO v_caller_role
    FROM organizations.members
    WHERE org_id = p_org_id AND lenser_id = v_caller_id;

    IF v_caller_role IS NULL OR v_caller_role NOT IN ('owner', 'admin') THEN
        RAISE EXCEPTION 'Only org owners and admins can invite members';
    END IF;

    -- Validate role
    IF p_role NOT IN ('admin', 'manager', 'billing', 'member', 'viewer') THEN
        RAISE EXCEPTION 'Invalid role: %. Cannot assign owner role via invite.', p_role;
    END IF;

    -- Check target lenser exists
    IF NOT EXISTS (SELECT 1 FROM lensers.profiles WHERE id = p_lenser_id) THEN
        RAISE EXCEPTION 'Lenser not found: %', p_lenser_id;
    END IF;

    -- Check not already a member
    IF EXISTS (SELECT 1 FROM organizations.members WHERE org_id = p_org_id AND lenser_id = p_lenser_id) THEN
        RAISE EXCEPTION 'Lenser is already a member of this organization';
    END IF;

    -- Insert member
    INSERT INTO organizations.members (org_id, lenser_id, role, invited_by)
    VALUES (p_org_id, p_lenser_id, p_role::organizations.member_role_enum, v_caller_id)
    RETURNING * INTO v_member;

    -- Log invite
    INSERT INTO organizations.audit_logs (org_id, lenser_id, action, entity_type, entity_id, metadata)
    VALUES (p_org_id, v_caller_id, 'member_added', 'member', p_lenser_id, jsonb_build_object(
        'invited_lenser_id', p_lenser_id,
        'role', p_role
    ));

    RETURN v_member;
END;
$$;


ALTER FUNCTION "organizations"."fn_invite_member"("p_org_id" "uuid", "p_lenser_id" "uuid", "p_role" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "organizations"."fn_invite_member"("p_org_id" "uuid", "p_lenser_id" "uuid", "p_role" "text") IS 'SECURITY DEFINER helper to invite a member to an organization. Validates caller is owner/admin, target lenser exists, and is not already a member. Cannot assign owner role via invite. Logs the invitation to audit_logs.';



CREATE OR REPLACE FUNCTION "organizations"."trg_audit_logs_immutable"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'organizations'
    AS $$
BEGIN
    IF TG_OP = 'DELETE' THEN
        RAISE EXCEPTION 'Organization audit logs are immutable. Deletion is not permitted.';
    ELSIF TG_OP = 'UPDATE' THEN
        RAISE EXCEPTION 'Organization audit logs are immutable. Updates are not permitted.';
    END IF;
    RETURN NULL;
END;
$$;


ALTER FUNCTION "organizations"."trg_audit_logs_immutable"() OWNER TO "postgres";


COMMENT ON FUNCTION "organizations"."trg_audit_logs_immutable"() IS 'Defense-in-depth trigger preventing UPDATE/DELETE on organizations.audit_logs. Mirrors wallet.trg_transactions_immutable() pattern.';



CREATE OR REPLACE FUNCTION "organizations"."trg_audit_membership"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
    PERFORM audit.log_event_v2(
        TG_OP::text,                                            -- event_type
        'lenser',                                                -- actor_type
        COALESCE(NEW.lenser_id, OLD.lenser_id),                 -- actor_id
        'organizations',                                         -- entity_schema
        'members',                                               -- entity_table
        COALESCE(NEW.org_id, OLD.org_id),                       -- entity_id
        jsonb_build_object(
            'lenser_id', COALESCE(NEW.lenser_id, OLD.lenser_id),
            'role', COALESCE(NEW.role::text, OLD.role::text),
            'operation', TG_OP
        ),                                                       -- payload
        'info'                                                   -- severity
    );
    RETURN COALESCE(NEW, OLD);
END;
$$;


ALTER FUNCTION "organizations"."trg_audit_membership"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "platform"."fn_get_platform_system_flags"() RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'public'
    AS $$
  SELECT jsonb_object_agg(key, (value::boolean))
  FROM   platform.system_flags
  WHERE  key IN (
    'autonomy_dispatch_enabled',
    'public_battles_enabled',
    'webhook_outbox_enabled'
  )
$$;


ALTER FUNCTION "platform"."fn_get_platform_system_flags"() OWNER TO "postgres";


COMMENT ON FUNCTION "platform"."fn_get_platform_system_flags"() IS 'Return a jsonb map of the three core platform.system_flags as booleans. service_role only.';



CREATE OR REPLACE FUNCTION "platform"."fn_get_worker_health"() RETURNS TABLE("worker_id" "text", "worker_type" "text", "last_seen_at" timestamp with time zone, "is_healthy" boolean, "seconds_since" numeric)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'platform'
    AS $$
  SELECT
    worker_id,
    worker_type,
    last_seen_at,
    (EXTRACT(EPOCH FROM (now() - last_seen_at)) < 30) AS is_healthy,
    ROUND(EXTRACT(EPOCH FROM (now() - last_seen_at))::NUMERIC, 1) AS seconds_since
  FROM platform.api_worker_heartbeats
  ORDER BY last_seen_at DESC;
$$;


ALTER FUNCTION "platform"."fn_get_worker_health"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "platform"."fn_queue_stats"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'lenses', 'automation', 'audit', 'public'
    AS $$
DECLARE
  v_in_flight        integer;
  v_automation       integer;
  v_scheduled        integer;
  v_outbox_pending   integer;
  v_outbox_dead      integer;
  v_autonomy_enabled boolean;
BEGIN
  SELECT COUNT(*) INTO v_in_flight
  FROM   lenses.workflow_runs
  WHERE  status IN ('pending', 'running');

  SELECT COUNT(*) INTO v_automation
  FROM   automation.events e
  LEFT JOIN automation.event_dispatches d ON d.event_id = e.id
  WHERE  d.event_id IS NULL;

  SELECT COUNT(*) INTO v_scheduled
  FROM   lenses.workflow_schedules
  WHERE  is_active = true
    AND  (last_run_at IS NULL OR last_run_at < now() - interval '5 minutes');

  SELECT COUNT(*) INTO v_outbox_pending
  FROM   audit.webhook_outbox
  WHERE  delivered_at IS NULL
    AND  dead_lettered_at IS NULL;

  SELECT COUNT(*) INTO v_outbox_dead
  FROM   audit.webhook_outbox
  WHERE  dead_lettered_at IS NOT NULL;

  SELECT COALESCE((value)::text::boolean, true) INTO v_autonomy_enabled
  FROM   platform.system_flags
  WHERE  key = 'autonomy_dispatch_enabled';

  IF v_autonomy_enabled IS NULL THEN
    -- No row -> treat as enabled (matches dispatch-gate default).
    v_autonomy_enabled := true;
  END IF;

  RETURN jsonb_build_object(
    'workers',            jsonb_build_object('active', 0, 'idle', 0),
    'in_flight_runs',     v_in_flight,
    'dispatcher_backlog', jsonb_build_object(
      'automation',                 v_automation,
      'scheduled',                  v_scheduled,
      'webhook_outbox_undelivered', v_outbox_pending,
      'webhook_outbox_dead_lettered', v_outbox_dead
    ),
    'system_flags', jsonb_build_object(
      'autonomy_dispatch_enabled', v_autonomy_enabled
    )
  );
END;
$$;


ALTER FUNCTION "platform"."fn_queue_stats"() OWNER TO "postgres";


COMMENT ON FUNCTION "platform"."fn_queue_stats"() IS 'Phase Y5: admin-only platform health snapshot (in-flight runs, dispatcher backlog, webhook outbox health, autonomy_dispatch_enabled flag). service_role only.';



CREATE OR REPLACE FUNCTION "platform"."fn_recent_audit_events"("p_since" interval DEFAULT '00:05:00'::interval) RETURNS TABLE("occurred_at" timestamp with time zone, "source" "text", "kind" "text", "summary" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'platform', 'audit', 'agents', 'automation', 'public'
    AS $$
  WITH cutoff AS (SELECT now() - p_since AS t)
  SELECT * FROM (
    SELECT
      md.occurred_at                                                AS occurred_at,
      'audit.moderation_decisions'::text                            AS source,
      'moderation'::text                                            AS kind,
      (md.decision_type
        || COALESCE(' ' || left(md.target_entity_id::text, 8), ''))::text AS summary
    FROM audit.moderation_decisions md, cutoff
    WHERE md.occurred_at > cutoff.t

    UNION ALL

    SELECT
      COALESCE(wo.dead_lettered_at, wo.created_at)                  AS occurred_at,
      'audit.webhook_outbox'::text                                  AS source,
      'webhook_failure'::text                                       AS kind,
      COALESCE(wo.last_error, '(no error captured)')                AS summary
    FROM audit.webhook_outbox wo, cutoff
    WHERE (wo.dead_lettered_at IS NOT NULL AND wo.dead_lettered_at > cutoff.t)
       OR (wo.delivered_at IS NULL AND wo.last_error IS NOT NULL AND wo.created_at > cutoff.t)

    UNION ALL

    SELECT
      al.occurred_at                                                AS occurred_at,
      'agents.action_logs'::text                                    AS source,
      'action'::text                                                AS kind,
      al.action_type                                                AS summary
    FROM agents.action_logs al, cutoff
    WHERE al.occurred_at > cutoff.t

    UNION ALL

    SELECT
      ev.occurred_at                                                AS occurred_at,
      'automation.events'::text                                     AS source,
      'automation_event'::text                                      AS kind,
      ev.event_type                                                 AS summary
    FROM automation.events ev, cutoff
    WHERE ev.occurred_at > cutoff.t
  ) merged
  ORDER BY occurred_at DESC
  LIMIT 200;
$$;


ALTER FUNCTION "platform"."fn_recent_audit_events"("p_since" interval) OWNER TO "postgres";


COMMENT ON FUNCTION "platform"."fn_recent_audit_events"("p_since" interval) IS 'Phase Y5: admin-only flat stream of recent audit + automation + agent action events for tail-logs. Capped at 200 rows. service_role only.';



CREATE OR REPLACE FUNCTION "platform"."fn_set_autonomy_dispatch_enabled"("p_enabled" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'public'
    AS $$
BEGIN
  IF p_enabled IS NULL THEN
    RAISE EXCEPTION 'p_enabled must not be NULL.' USING ERRCODE = '22023';
  END IF;

  INSERT INTO platform.system_flags (key, value, updated_at)
  VALUES ('autonomy_dispatch_enabled', to_jsonb(p_enabled), now())
  ON CONFLICT (key) DO UPDATE
    SET value      = EXCLUDED.value,
        updated_at = EXCLUDED.updated_at;
END;
$$;


ALTER FUNCTION "platform"."fn_set_autonomy_dispatch_enabled"("p_enabled" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "platform"."fn_set_autonomy_dispatch_enabled"("p_enabled" boolean) IS 'Phase Y5: flip platform.system_flags.autonomy_dispatch_enabled. Backs `lf platform pause-dispatch` / `resume-dispatch`. service_role only.';



CREATE OR REPLACE FUNCTION "platform"."fn_set_platform_flag"("p_key" "text", "p_enabled" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'public'
    AS $$
BEGIN
  IF p_key NOT IN ('autonomy_dispatch_enabled', 'public_battles_enabled', 'webhook_outbox_enabled') THEN
    RAISE EXCEPTION 'Unknown platform flag: %', p_key USING ERRCODE = 'invalid_parameter_value';
  END IF;

  INSERT INTO platform.system_flags (key, value, updated_at)
  VALUES (p_key, p_enabled::text, now())
  ON CONFLICT (key) DO UPDATE
    SET value      = EXCLUDED.value,
        updated_at = now();
END;
$$;


ALTER FUNCTION "platform"."fn_set_platform_flag"("p_key" "text", "p_enabled" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "platform"."fn_set_platform_flag"("p_key" "text", "p_enabled" boolean) IS 'Toggle one of the three core platform.system_flags. service_role only. Allowed keys: autonomy_dispatch_enabled, public_battles_enabled, webhook_outbox_enabled.';



CREATE OR REPLACE FUNCTION "platform"."fn_upsert_worker_heartbeat"("p_worker_id" "text", "p_worker_type" "text" DEFAULT 'workflow'::"text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform'
    AS $$
BEGIN
  INSERT INTO platform.api_worker_heartbeats (worker_id, worker_type, last_seen_at, metadata)
  VALUES (p_worker_id, p_worker_type, now(), p_metadata)
  ON CONFLICT (worker_id) DO UPDATE
  SET last_seen_at = now(),
      worker_type  = EXCLUDED.worker_type,
      metadata     = EXCLUDED.metadata;
END;
$$;


ALTER FUNCTION "platform"."fn_upsert_worker_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint DEFAULT 0, "p_output_tokens" bigint DEFAULT 0, "p_units" integer DEFAULT 1) RETURNS bigint
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  select "billing"."calculate_credit_cost"(p_model_id, p_input_tokens, p_output_tokens, p_units);
$$;


ALTER FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."custom_access_token_hook"("event" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
  v_claims    jsonb;
BEGIN
  v_claims := event -> 'claims';

  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = (event ->> 'user_id')::uuid
    AND type    = 'human'
  LIMIT 1;

  IF v_lenser_id IS NOT NULL THEN
    v_claims := jsonb_set(v_claims, '{lenser_id}', to_jsonb(v_lenser_id));
  END IF;

  RETURN jsonb_set(event, '{claims}', v_claims);
END;
$$;


ALTER FUNCTION "public"."custom_access_token_hook"("event" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") IS 'Supabase custom access token hook. Embeds lenser_id (human profile UUID) into JWT claims. Register in Dashboard → Auth → Hooks → Custom Access Token.';



CREATE OR REPLACE FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") RETURNS "text"
    LANGUAGE "sql" IMMUTABLE
    SET "search_path" TO 'public'
    AS $_$
  SELECT CASE p_table_name
    WHEN 'teams'                THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'evaluations'          THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'memory_profiles'      THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'model_profiles'       THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'personality_profiles' THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'scratchpad_runs'      THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'tool_assignments'     THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'tool_profiles'        THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'workflow_assignments' THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'workspace_settings'   THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'team_members'         THEN 'EXISTS (SELECT 1 FROM agents.teams t JOIN agents.ownerships o ON o.ai_lenser_id = t.ai_lenser_id WHERE t.id = tbl.team_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'team_edges'           THEN 'EXISTS (SELECT 1 FROM agents.teams t JOIN agents.ownerships o ON o.ai_lenser_id = t.ai_lenser_id WHERE t.id = tbl.team_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'agent_run_events'     THEN 'EXISTS (SELECT 1 FROM agents.team_runs tr JOIN agents.ownerships o ON o.ai_lenser_id = tr.ai_lenser_id WHERE tr.id = tbl.team_run_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'agent_run_steps'      THEN 'EXISTS (SELECT 1 FROM agents.team_runs tr JOIN agents.ownerships o ON o.ai_lenser_id = tr.ai_lenser_id WHERE tr.id = tbl.team_run_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'tools_registry'       THEN 'tbl.owner_lenser_id = $2'
    WHEN 'evaluation_cases'     THEN 'EXISTS (SELECT 1 FROM agents.evaluations e JOIN agents.ownerships o ON o.ai_lenser_id = e.ai_lenser_id WHERE e.id = tbl.evaluation_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'evaluation_rubrics'   THEN 'EXISTS (SELECT 1 FROM agents.evaluations e JOIN agents.ownerships o ON o.ai_lenser_id = e.ai_lenser_id WHERE e.id = tbl.evaluation_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'evaluation_baselines' THEN 'EXISTS (SELECT 1 FROM agents.evaluations e JOIN agents.ownerships o ON o.ai_lenser_id = e.ai_lenser_id WHERE e.id = tbl.evaluation_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
  END
$_$;


ALTER FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") IS 'Internal helper: returns a parameterized SQL predicate expressing ownership of an agents.<table> row by lenser id $2. Used by fn_delete_workspace_item / fn_upsert_workspace_item to dispatch per-table ownership shapes safely.';



CREATE OR REPLACE FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_target_id uuid;
BEGIN
  SELECT id INTO v_target_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_target_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.relationships
  SET status = 'accepted',
      responded_at = now(),
      accepted_at  = now()
  WHERE source_profile_id = p_source_profile_id
    AND target_profile_id = v_target_id
    AND status = 'pending';

  IF NOT FOUND THEN
    RETURN jsonb_build_object('success', false, 'reason', 'no_pending_request');
  END IF;

  RETURN jsonb_build_object('success', true);
END;
$$;


ALTER FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean DEFAULT true) RETURNS TABLE("id" "uuid", "job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "error_code" "text", "error_message" "text", "attempt_count" integer, "payload" "jsonb", "resolved_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  RETURN QUERY
    SELECT
      d.id, d.job_id, d.battle_id, d.contender_id, d.slot,
      d.error_code, d.error_message, d.attempt_count, d.payload,
      d.resolved_at, d.created_at
    FROM battles.battle_execution_dead_letters d
    WHERE (NOT p_unresolved_only OR d.resolved_at IS NULL)
    ORDER BY d.created_at DESC
    LIMIT 200;
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer DEFAULT 30) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "updated_at" timestamp with time zone, "stale_seconds" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  RETURN QUERY
    SELECT
      b.id,
      b.slug,
      b.title,
      b.status::TEXT,
      b.updated_at,
      ROUND(EXTRACT(EPOCH FROM (now() - b.updated_at))::NUMERIC, 1) AS stale_seconds
    FROM battles.battles b
    WHERE b.status IN ('executing', 'scoring')
      AND b.updated_at < now() - (p_threshold_minutes || ' minutes')::INTERVAL
    ORDER BY b.updated_at ASC;
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "audit"."vote_anomalies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "voter_lenser_id" "uuid" NOT NULL,
    "anomaly_type" "text" NOT NULL,
    "score" real DEFAULT 0 NOT NULL,
    "detected_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "resolved_at" timestamp with time zone,
    "resolved_by" "uuid"
);


ALTER TABLE "audit"."vote_anomalies" OWNER TO "postgres";


COMMENT ON TABLE "audit"."vote_anomalies" IS 'CA: Vote manipulation anomaly records. Populated by service_role worker; resolved by super_admin only.';



CREATE OR REPLACE FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text" DEFAULT 'pending'::"text", "p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 100) RETURNS SETOF "audit"."vote_anomalies"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'Forbidden: super_admin required' USING ERRCODE = 'P0003';
  END IF;

  RETURN QUERY
    SELECT *
      FROM audit.vote_anomalies
     WHERE (
       p_status = 'all'
       OR (p_status = 'pending'  AND resolved_at IS NULL)
       OR (p_status = 'resolved' AND resolved_at IS NOT NULL)
     )
       AND (p_battle_id IS NULL OR battle_id = p_battle_id)
     ORDER BY detected_at DESC
     LIMIT LEAST(p_limit, 500);
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) IS 'CA: Super-admin list helper for vote anomalies. Used by CLI and admin UI.';



CREATE OR REPLACE FUNCTION "public"."fn_admin_get_worker_health"() RETURNS TABLE("worker_id" "text", "worker_type" "text", "last_seen_at" timestamp with time zone, "is_healthy" boolean, "seconds_since" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  RETURN QUERY SELECT * FROM platform.fn_get_worker_health();
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_worker_health"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_health"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'cron', 'public'
    AS $$
DECLARE
  v_crons jsonb;
BEGIN
  SELECT jsonb_agg(jsonb_build_object(
    'name',      j.jobname,
    'schedule',  j.schedule,
    'active',    j.active,
    'last_run',  (
      SELECT start_time
      FROM   cron.job_run_details d
      WHERE  d.jobid = j.jobid
      ORDER  BY start_time DESC
      LIMIT  1
    )
  ))
  INTO v_crons
  FROM cron.job j;

  RETURN jsonb_build_object('crons', COALESCE(v_crons, '[]'::jsonb));
END;
$$;


ALTER FUNCTION "public"."fn_admin_health"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_admin_health"() IS 'Phase AI: returns pg_cron job list with last-run timestamps. Restricted to service_role. Used by health:cron gate script.';



CREATE OR REPLACE FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  PERFORM battles.fn_retry_dead_letter_battle_job(p_dead_letter_id);
END;
$$;


ALTER FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."series" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "title" "text" NOT NULL,
    "template_id" "uuid" NOT NULL,
    "creator_lenser_id" "uuid" NOT NULL,
    "round_count" integer DEFAULT 3 NOT NULL,
    "current_round" integer DEFAULT 1 NOT NULL,
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "series_current_round_check" CHECK (("current_round" >= 1)),
    CONSTRAINT "series_round_count_check" CHECK ((("round_count" >= 1) AND ("round_count" <= 16))),
    CONSTRAINT "series_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'complete'::"text"]))),
    CONSTRAINT "series_title_check" CHECK ((("char_length"("title") >= 1) AND ("char_length"("title") <= 200)))
);


ALTER TABLE "battles"."series" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") RETURNS "battles"."series"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid       UUID := auth.uid();
  v_series    battles.series%ROWTYPE;
  v_current   battles.series_rounds%ROWTYPE;
  v_winner    UUID;
  v_battle_id UUID;
  v_slug      TEXT;
  v_next      INT;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_series FROM battles.series WHERE id = p_series_id;
  IF v_series IS NULL THEN
    RAISE EXCEPTION 'series_not_found' USING ERRCODE = '42501';
  END IF;
  IF v_series.creator_lenser_id <> v_uid THEN
    RAISE EXCEPTION 'series_not_owned' USING ERRCODE = '42501';
  END IF;
  IF v_series.status = 'complete' THEN
    RETURN v_series;
  END IF;

  SELECT * INTO v_current
    FROM battles.series_rounds
   WHERE series_id    = p_series_id
     AND round_number = v_series.current_round;

  -- Resolve the winner from battles.battles.winner_contender_id; stamp it
  -- on the series_rounds row as the canonical record.
  SELECT b.winner_contender_id INTO v_winner
    FROM battles.battles b
   WHERE b.id = v_current.battle_id;
  IF v_winner IS NULL THEN
    RAISE EXCEPTION 'current_round_has_no_winner' USING ERRCODE = '22023';
  END IF;

  UPDATE battles.series_rounds
     SET winner_contender_id = v_winner
   WHERE id = v_current.id;

  v_next := v_series.current_round + 1;
  IF v_next > v_series.round_count THEN
    UPDATE battles.series
       SET status     = 'complete',
           updated_at = now()
     WHERE id = p_series_id
     RETURNING * INTO v_series;
    RETURN v_series;
  END IF;

  v_slug := lower(regexp_replace(v_series.title, '[^a-zA-Z0-9]+', '-', 'g'))
            || '-r' || v_next::text || '-' || substr(p_series_id::text, 1, 8);

  v_battle_id := public.fn_battles_create_from_template(
    v_series.template_id,
    v_series.title || ' — Round ' || v_next::text,
    v_slug
  );

  INSERT INTO battles.series_rounds (series_id, round_number, battle_id)
  VALUES (p_series_id, v_next, v_battle_id);

  UPDATE battles.series
     SET current_round = v_next,
         updated_at    = now()
   WHERE id = p_series_id
   RETURNING * INTO v_series;

  RETURN v_series;
END $$;


ALTER FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
BEGIN
  PERFORM battles.fn_advance_tournament(p_match_id);
END;
$$;


ALTER FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
    v_lenser_id  uuid;
    v_adapter_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'NOT_AUTHENTICATED';
    END IF;

    INSERT INTO agents.adapters (owner_lenser_id, name, adapter_type, config)
    VALUES (v_lenser_id, p_name, p_adapter_type, p_config)
    RETURNING id INTO v_adapter_id;

    RETURN v_adapter_id;
END;
$$;


ALTER FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'NOT_AUTHENTICATED';
    END IF;

    IF NOT EXISTS (
        SELECT 1 FROM agents.adapters
        WHERE id = p_adapter_id AND owner_lenser_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'NOT_FOUND_OR_FORBIDDEN';
    END IF;

    UPDATE agents.adapters
    SET is_active = false, updated_at = now()
    WHERE id = p_adapter_id AND owner_lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") RETURNS integer
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'audit', 'public'
    AS $$
  SELECT count(*)::INT
  FROM   audit.events
  WHERE  event_type = 'battle.joined'
  AND    payload->>'agent_id' = p_agent_id::TEXT
  AND    occurred_at >= date_trunc('day', now() AT TIME ZONE 'UTC');
$$;


ALTER FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") RETURNS TABLE("id" "uuid", "category" "text", "execution_mode" "text", "workflow_id" "uuid", "require_owner_approval" boolean, "max_joins_per_day" integer, "active" boolean, "daily_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
  SELECT
    bs.id,
    bs.category,
    bs.execution_mode,
    bs.workflow_id,
    bs.require_owner_approval,
    bs.max_joins_per_day,
    bs.active,
    public.fn_agent_daily_join_count(p_agent_id) AS daily_count,
    bs.created_at
  FROM  agents.battle_subscriptions bs
  JOIN  agents.ownerships o ON o.ai_lenser_id = bs.agent_id
  JOIN  lensers.profiles   p ON p.id           = o.owner_lenser_id
  WHERE bs.agent_id = p_agent_id
  AND   o.role IN ('owner', 'co_owner')
  AND   (o.revoked_at IS NULL OR o.revoked_at > now())
  AND   p.user_id = auth.uid()
  ORDER BY bs.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid" DEFAULT NULL::"uuid", "p_event_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "team_run_id" "uuid", "agent_run_step_id" "uuid", "event_type" "text", "payload" "jsonb", "occurred_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  -- Ownership guard: caller must be the owner or co-owner of this AI lenser.
  -- can_manage_ai_lenser resolves via lensers.get_auth_human_lenser_id(),
  -- which handles workspace-switching (agent_owner mode).
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    ev.id,
    ev.team_run_id,
    ev.agent_run_step_id,
    ev.event_type,
    ev.payload,
    ev.occurred_at
  FROM   agents.agent_run_events ev
  JOIN   agents.team_runs        tr ON tr.id = ev.team_run_id
  WHERE  tr.ai_lenser_id = p_ai_lenser_id
    AND  (p_run_id     IS NULL OR ev.team_run_id = p_run_id)
    AND  (p_event_type IS NULL OR ev.event_type  = p_event_type)
  ORDER BY ev.occurred_at DESC
  LIMIT  GREATEST(1, LEAST(COALESCE(p_limit, 100), 500));
END;
$$;


ALTER FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) IS 'Owner-only event stream for agents.agent_run_events. Filters by ai_lenser_id via team_runs join. Optional p_run_id and p_event_type narrowing. Authorization via agents.can_manage_ai_lenser (human owner profile). Max 500 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text" DEFAULT NULL::"text", "p_execution_mode" "text" DEFAULT 'cloud'::"text", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_require_approval" boolean DEFAULT false, "p_max_joins_per_day" integer DEFAULT 5) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_sub_id    UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships
    WHERE  ai_lenser_id    = p_agent_id
    AND    owner_lenser_id = v_lenser_id
    AND    role IN ('owner', 'co_owner')
  ) THEN
    RAISE EXCEPTION 'subscribe_forbidden: agent not owned by caller';
  END IF;

  INSERT INTO agents.battle_subscriptions (
    agent_id, category, execution_mode, workflow_id,
    require_owner_approval, max_joins_per_day
  )
  VALUES (
    p_agent_id, p_category, p_execution_mode, p_workflow_id,
    p_require_approval, p_max_joins_per_day
  )
  RETURNING id INTO v_sub_id;

  RETURN v_sub_id;
END;
$$;


ALTER FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  UPDATE agents.battle_subscriptions bs
  SET    active     = false,
         updated_at = now()
  FROM   agents.ai_lensers al
  JOIN   agents.ownerships  o ON o.ai_lenser_id    = al.id
                              AND o.owner_lenser_id = v_lenser_id
                              AND o.role IN ('owner', 'co_owner')
  WHERE  bs.id    = p_subscription_id
  AND    al.id    = bs.agent_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'unsubscribe_not_found_or_forbidden: %', p_subscription_id;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") RETURNS TABLE("id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "key" "text", "name" "text", "description" "text", "docs_url" "text", "support_level" "text", "status" "text", "capabilities" "text"[], "input_modalities" "text"[], "output_modalities" "text"[], "context_window_tokens" integer, "supports_tools" boolean, "supports_json_schema" boolean, "supports_vision" boolean, "supports_streaming" boolean, "use_cases" "text"[], "developer_summary" "text", "user_summary" "text", "metadata" "jsonb", "unit_type" "text", "cost_per_unit" numeric, "input_cost_per_1k_tokens" numeric, "output_cost_per_1k_tokens" numeric, "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT *
  FROM public.fn_ai_catalog_models(p_provider_key, NULL, NULL, NULL)
  WHERE key = p_model_key
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text" DEFAULT NULL::"text", "p_support_level" "text" DEFAULT NULL::"text", "p_capability" "text" DEFAULT NULL::"text", "p_modality" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "key" "text", "name" "text", "description" "text", "docs_url" "text", "support_level" "text", "status" "text", "capabilities" "text"[], "input_modalities" "text"[], "output_modalities" "text"[], "context_window_tokens" integer, "supports_tools" boolean, "supports_json_schema" boolean, "supports_vision" boolean, "supports_streaming" boolean, "use_cases" "text"[], "developer_summary" "text", "user_summary" "text", "metadata" "jsonb", "unit_type" "text", "cost_per_unit" numeric, "input_cost_per_1k_tokens" numeric, "output_cost_per_1k_tokens" numeric, "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT
    m.id,
    m.provider_id,
    p.key AS provider_key,
    p.display_name AS provider_name,
    m.key,
    m.name,
    m.description,
    COALESCE(m.docs_url, m.provider_url),
    m.support_level,
    m.status,
    m.capabilities,
    m.input_modalities,
    m.output_modalities,
    m.context_window_tokens,
    m.supports_tools,
    m.supports_json_schema,
    m.supports_vision,
    m.supports_streaming,
    m.use_cases,
    m.developer_summary,
    m.user_summary,
    m.metadata,
    pricing.unit_type,
    pricing.cost_per_unit,
    pricing.input_cost_per_1k_tokens,
    pricing.output_cost_per_1k_tokens,
    m.is_active
  FROM ai.models m
  JOIN ai.providers p ON p.id = m.provider_id
  LEFT JOIN LATERAL (
    SELECT
      mp.unit_type::text AS unit_type,
      mp.cost_per_unit,
      mp.input_cost_per_1k_tokens,
      mp.output_cost_per_1k_tokens
    FROM ai.model_pricing mp
    WHERE mp.model_id = m.id
      AND mp.effective_from <= now()
      AND (mp.effective_to IS NULL OR mp.effective_to > now())
    ORDER BY mp.effective_from DESC
    LIMIT 1
  ) AS pricing ON TRUE
  WHERE (p_provider_key IS NULL OR p.key = p_provider_key)
    AND (p_support_level IS NULL OR m.support_level = p_support_level)
    AND (p_capability IS NULL OR p_capability = ANY (m.capabilities))
    AND (
      p_modality IS NULL OR
      p_modality = ANY (m.input_modalities) OR
      p_modality = ANY (m.output_modalities)
    )
  ORDER BY
    CASE m.support_level
      WHEN 'runnable' THEN 0
      WHEN 'byok_only' THEN 1
      WHEN 'catalog_only' THEN 2
      ELSE 3
    END,
    p.display_name,
    m.name;
$$;


ALTER FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_catalog_providers"() RETURNS TABLE("id" "uuid", "key" "text", "display_name" "text", "base_url" "text", "docs_url" "text", "support_level" "text", "logo_slug" "text", "metadata" "jsonb", "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT
    p.id,
    p.key,
    p.display_name,
    p.base_url,
    p.docs_url,
    p.support_level,
    p.logo_slug,
    p.metadata,
    p.is_active
  FROM ai.providers p
  ORDER BY
    CASE p.support_level
      WHEN 'runnable' THEN 0
      WHEN 'byok_only' THEN 1
      WHEN 'catalog_only' THEN 2
      ELSE 3
    END,
    p.display_name;
$$;


ALTER FUNCTION "public"."fn_ai_catalog_providers"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0, "p_media_kind" "text" DEFAULT NULL::"text", "p_ai_model_slug" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "ai_model_slug" "text", "lens_id" "uuid", "input_text" "text", "input_params" "jsonb", "output_type" "text", "visibility" "content"."visibility_enum", "created_at" timestamp with time zone, "original_chat_url" "text", "media" "jsonb")
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'ai', 'content', 'lensers', 'auth'
    AS $$
  select
    g.id,
    m.slug as ai_model_slug,
    g.lens_id,
    g.input_text,
    g.input_params,
    g.output_type,
    g.visibility,
    g.created_at,
    g.original_chat_url,
    jsonb_build_object(
      'id', ml.id,
      'url', ml.url,
      'storage_bucket', ml.storage_bucket,
      'file_name', ml.file_name,
      'display_name', ml.display_name,
      'mime_type', ml.mime_type,
      'extension', ml.extension,
      'size_bytes', ml.size_bytes,
      'width', ml.width,
      'height', ml.height,
      'duration_seconds', ml.duration_seconds,
      'media_kind', ml.media_kind,
      'source', ml.source,
      'meta', ml.meta,
      'created_at', ml.created_at
    ) as media
  from ai.generations g
  join ai.models m
    on m.id = g.ai_model_id
  join content.media_library ml
    on ml.id = g.media_id
  where
    g.lens_id = p_lens_id
    and g.lenser_id = p_lenser_id
    and (p_ai_model_slug is null or m.slug = p_ai_model_slug)
    and (p_media_kind is null or ml.media_kind = p_media_kind)
  order by g.created_at desc
  limit p_limit
  offset p_offset;
$$;


ALTER FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 5) RETURNS TABLE("product_tag" "text", "page" "text", "message" "text", "start_date" timestamp with time zone, "end_date" timestamp with time zone, "status" "text", "created_at" timestamp with time zone, "total_count" bigint)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'auth'
    AS $$
begin
  return query
  select
    f.product_tag::text,
    f.page,
    f.message,
    f.start_date,
    f.end_date,
    f.status::text,
    f.created_at,
    count(*) over() as total_count
  from analytics.product_feedback f
  where f.user_id = auth.uid()
  order by f.created_at desc
  offset p_offset
  limit p_limit;
end;
$$;


ALTER FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text" DEFAULT 'opened'::"text", "p_ip_hash" "text" DEFAULT NULL::"text", "p_user_agent" "text" DEFAULT NULL::"text", "p_referer" "text" DEFAULT NULL::"text", "p_country" "text" DEFAULT NULL::"text", "p_city" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'lensers', 'auth'
    AS $$
DECLARE
  v_user_id uuid;
  v_lenser_id uuid;
  v_link_id uuid;
BEGIN
  ------------------------------------------------------------
  -- Extract JWT user_id (NULL if anonymous)
  ------------------------------------------------------------
  BEGIN
    v_user_id := (auth.jwt() ->> 'sub')::uuid;
  EXCEPTION WHEN OTHERS THEN
    v_user_id := NULL;
  END;

  ------------------------------------------------------------
  -- Detect Lenser profile
  ------------------------------------------------------------
  IF v_user_id IS NOT NULL THEN
    SELECT id INTO v_lenser_id
    FROM lensers.profiles
    WHERE user_id = v_user_id;
  END IF;

  ------------------------------------------------------------
  -- Get link id from short_id
  ------------------------------------------------------------
  SELECT id INTO v_link_id
  FROM analytics.shared_links
  WHERE short_id = p_short_id;

  IF v_link_id IS NULL THEN
    RAISE EXCEPTION 'Shared link not found';
  END IF;

  ------------------------------------------------------------
  -- Insert event
  ------------------------------------------------------------
  INSERT INTO analytics.share_events (
    shared_link_id,
    event_type,
    viewer_lenser_id,
    ip_hash,
    user_agent,
    referer,
    country,
    city
  )
  VALUES (
    v_link_id,
    p_event_type,
    v_lenser_id,
    p_ip_hash,
    p_user_agent,
    p_referer,
    p_country,
    p_city
  );

END;
$$;


ALTER FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "analytics"."shared_links" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "short_id" "text" NOT NULL,
    "resource_type" "text" NOT NULL,
    "resource_id" "uuid",
    "slug" "text",
    "creator_lenser_id" "uuid",
    "channel" "text" DEFAULT 'in_app'::"text" NOT NULL,
    "campaign_key" "text",
    "experiment_key" "text",
    "experiment_variant" "text",
    "meta" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "display_name" "text",
    "expires_at" timestamp with time zone,
    "max_uses" integer,
    "use_count" integer DEFAULT 0 NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    CONSTRAINT "ck_analytics_shared_links_use_count_nonneg" CHECK ((("use_count" IS NULL) OR ("use_count" >= 0)))
);


ALTER TABLE "analytics"."shared_links" OWNER TO "postgres";


COMMENT ON TABLE "analytics"."shared_links" IS 'Configuration for shareable short links in LenserFight (per resource, per Lenser, per campaign).';



COMMENT ON COLUMN "analytics"."shared_links"."creator_lenser_id" IS 'FK to lensers.id; used for RLS (each Lenser sees only their own links).';



COMMENT ON COLUMN "analytics"."shared_links"."expires_at" IS 'When this link expires. NULL = no expiry. Used for invitation links.';



COMMENT ON COLUMN "analytics"."shared_links"."max_uses" IS 'Maximum number of times this link can be consumed. NULL = unlimited.';



COMMENT ON COLUMN "analytics"."shared_links"."use_count" IS 'Number of times this link has been consumed. Incremented by fn_analytics_shared_links_consume.';



COMMENT ON COLUMN "analytics"."shared_links"."is_active" IS 'Whether this link is active. Deactivated links return 410 Gone.';



CREATE OR REPLACE FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") RETURNS "analytics"."shared_links"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics'
    AS $$
DECLARE
    v_link analytics.shared_links;
BEGIN
    SELECT * INTO v_link
    FROM analytics.shared_links
    WHERE short_id = p_short_id;

    IF v_link.id IS NULL THEN
        RAISE EXCEPTION 'Shared link not found';
    END IF;

    IF NOT v_link.is_active THEN
        RAISE EXCEPTION 'Link is no longer active';
    END IF;

    IF v_link.expires_at IS NOT NULL AND now() > v_link.expires_at THEN
        UPDATE analytics.shared_links SET is_active = false WHERE id = v_link.id;
        RAISE EXCEPTION 'Link has expired';
    END IF;

    IF v_link.max_uses IS NOT NULL AND v_link.use_count >= v_link.max_uses THEN
        UPDATE analytics.shared_links SET is_active = false WHERE id = v_link.id;
        RAISE EXCEPTION 'Link usage limit reached';
    END IF;

    -- Increment use count
    UPDATE analytics.shared_links
    SET use_count = use_count + 1,
        is_active = CASE
            WHEN max_uses IS NOT NULL AND use_count + 1 >= max_uses THEN false
            ELSE is_active
        END
    WHERE id = v_link.id
    RETURNING * INTO v_link;

    RETURN v_link;
END;
$$;


ALTER FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text" DEFAULT NULL::"text", "p_slug" "text" DEFAULT NULL::"text", "p_channel" "text" DEFAULT 'in_app'::"text", "p_meta" "jsonb" DEFAULT '{}'::"jsonb", "p_display_name" "text" DEFAULT NULL::"text", "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_max_uses" integer DEFAULT NULL::integer) RETURNS "analytics"."shared_links"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'lensers', 'auth'
    AS $$
DECLARE
    v_user_id       uuid;
    v_lenser_id     uuid;
    v_resource_uuid uuid;
    v_short_id      text;
    v_new           analytics.shared_links;
    v_handle        text;
    v_recent_count  integer;
BEGIN
    -- 1. Identify the creator from JWT
    BEGIN
        v_user_id := (auth.jwt() ->> 'sub')::uuid;
    EXCEPTION WHEN OTHERS THEN
        v_user_id := NULL;
    END;

    IF v_user_id IS NOT NULL THEN
        SELECT id, handle INTO v_lenser_id, v_handle
        FROM lensers.profiles
        WHERE user_id = v_user_id;
    END IF;

    -- 2. Anti-abuse rate limiting
    SELECT COUNT(*) INTO v_recent_count
    FROM analytics.shared_links
    WHERE creator_lenser_id = v_lenser_id
      AND created_at > now() - interval '1 hour';

    IF v_recent_count >= 100 THEN
        RAISE EXCEPTION 'Rate limit exceeded: too many shared links created recently';
    END IF;

    -- 3. Validate resource_type
    IF p_resource_type NOT IN (
        'thread', 'lens', 'battle', 'profile', 'tag',
        'group_invite', 'org_member_invite', 'battle_invite',
        'workflow', 'page'
    ) THEN
        RAISE EXCEPTION 'Invalid resource_type: %', p_resource_type;
    END IF;

    -- 4. Parse and validate resource_id
    IF p_resource_id IS NOT NULL THEN
        BEGIN
            v_resource_uuid := p_resource_id::uuid;
        EXCEPTION WHEN OTHERS THEN
            RAISE EXCEPTION 'Invalid resource_id format: must be a valid UUID';
        END;

        -- Validate resource exists for invitation types
        IF p_resource_type = 'group_invite' THEN
            IF NOT EXISTS (SELECT 1 FROM lensers.groups WHERE id = v_resource_uuid) THEN
                RAISE EXCEPTION 'Group not found: %', v_resource_uuid;
            END IF;
        ELSIF p_resource_type = 'org_member_invite' THEN
            IF NOT EXISTS (SELECT 1 FROM organizations.organizations WHERE id = v_resource_uuid) THEN
                RAISE EXCEPTION 'Organization not found: %', v_resource_uuid;
            END IF;
        ELSIF p_resource_type = 'battle_invite' THEN
            IF NOT EXISTS (SELECT 1 FROM battles.battles WHERE id = v_resource_uuid) THEN
                RAISE EXCEPTION 'Battle not found: %', v_resource_uuid;
            END IF;
        END IF;
    END IF;

    -- 5. Generate short_id (using gen_random_uuid to avoid pgcrypto dependency)
    v_short_id := replace(gen_random_uuid()::text, '-', '');
    v_short_id := substr(v_short_id, 1, 8);

    -- 6. Insert shared link
    INSERT INTO analytics.shared_links (
        short_id, resource_type, resource_id, slug,
        creator_lenser_id, channel, meta, display_name,
        expires_at, max_uses
    )
    VALUES (
        v_short_id, p_resource_type, v_resource_uuid, p_slug,
        v_lenser_id, p_channel, p_meta,
        COALESCE(p_display_name, v_handle),
        p_expires_at, p_max_uses
    )
    RETURNING * INTO v_new;

    RETURN v_new;
END;
$$;


ALTER FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) IS 'Creates a shared or invitation link. Supports group_invite, org_member_invite, and battle_invite resource types. Added p_expires_at and p_max_uses in migration 000030. 6-param overload removed in migration 000054 to resolve PostgREST 42725 ambiguity. SECURITY DEFINER — rate-limited to 100 links/hour per lenser.';



CREATE OR REPLACE FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") RETURNS "analytics"."shared_links"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics'
    AS $$
DECLARE
  v_link analytics.shared_links;
BEGIN
  SELECT *
  INTO v_link
  FROM analytics.shared_links
  WHERE short_id = p_short_id;

  IF v_link.id IS NULL THEN
    RAISE EXCEPTION 'Shared link not found';
  END IF;

  RETURN v_link;
END;
$$;


ALTER FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text" DEFAULT 'general'::"text", "p_page" "text" DEFAULT NULL::"text", "p_message" "text" DEFAULT NULL::"text", "p_start_date" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_end_date" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'auth'
    AS $$
DECLARE
  v_user_id uuid;
BEGIN
  v_user_id := auth.uid();

  -- Must be authenticated
  IF v_user_id IS NULL THEN
    RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
  END IF;

  -- Message length guard (matches service-layer validation)
  IF p_message IS NOT NULL AND (length(trim(p_message)) < 10 OR length(trim(p_message)) > 2000) THEN
    RAISE EXCEPTION 'feedback message must be between 10 and 2000 characters' USING ERRCODE = '22023';
  END IF;

  -- Validate product_tag before cast to surface a clear error to the client
  IF p_product_tag NOT IN ('bug', 'feature', 'ui_ux', 'general', 'other') THEN
    RAISE EXCEPTION 'invalid product_tag: %. Must be one of: bug, feature, ui_ux, general, other', p_product_tag
      USING ERRCODE = '22023';
  END IF;

  INSERT INTO analytics.product_feedback (
    user_id,
    product_tag,
    page,
    message,
    start_date,
    end_date
  ) VALUES (
    v_user_id,
    p_product_tag::analytics.product_tag_enum,  -- FIX: explicit cast (42804)
    p_page,
    trim(p_message),
    p_start_date,
    p_end_date
  );
END;
$$;


ALTER FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) IS 'Inserts a product feedback row for the calling user. Validates message length and product_tag enum value server-side. Fixed in migration 000055: added explicit ::analytics.product_tag_enum cast and enum value guard to resolve 42804 type mismatch.';



CREATE OR REPLACE FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text" DEFAULT 'general'::"text", "p_page" "text" DEFAULT NULL::"text", "p_message" "text" DEFAULT NULL::"text", "p_start_date" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_end_date" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'auth'
    AS $$
DECLARE
  v_user_id uuid;
  v_recent_count int;
BEGIN
  -- Capture caller identity if authenticated (NULL is fine for anon)
  v_user_id := auth.uid();

  -- Message validation
  IF p_message IS NOT NULL AND (
    length(trim(p_message)) < 10 OR
    length(trim(p_message)) > 2000
  ) THEN
    RAISE EXCEPTION 'feedback message must be between 10 and 2000 characters'
      USING ERRCODE = '22023';
  END IF;

  -- Product tag validation
  IF p_product_tag NOT IN ('bug', 'feature', 'ui_ux', 'general', 'other') THEN
    RAISE EXCEPTION 'invalid product_tag: %. Must be one of: bug, feature, ui_ux, general, other',
      p_product_tag USING ERRCODE = '22023';
  END IF;

  -- Rate-limit authenticated users: max 5 per hour
  IF v_user_id IS NOT NULL THEN
    SELECT COUNT(*)
      INTO v_recent_count
      FROM analytics.product_feedback
     WHERE user_id = v_user_id
       AND created_at > now() - interval '1 hour';

    IF v_recent_count >= 5 THEN
      RAISE EXCEPTION 'rate limit exceeded: max 5 feedback submissions per hour'
        USING ERRCODE = '429';
    END IF;
  END IF;

  INSERT INTO analytics.product_feedback (
    user_id,
    product_tag,
    page,
    message,
    start_date,
    end_date
  ) VALUES (
    v_user_id,
    p_product_tag::analytics.product_tag_enum,
    p_page,
    trim(p_message),
    p_start_date,
    p_end_date
  );
END;
$$;


ALTER FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) IS 'Public (anon-accessible) feedback submission. Captures auth.uid() when authenticated. Rate-limits authenticated callers to 5/hour. Anon rate-limiting is handled client-side.';



CREATE OR REPLACE FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
  VALUES (p_team_run_id, p_event_type, p_payload, now());
$$;


ALTER FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("event_id" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_caller_id uuid := lensers.get_auth_lenser_id();
  v_next_id bigint;
  v_created_at timestamptz;
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    WHERE r.id = p_run_id
      AND r.triggered_by = v_caller_id
  ) THEN
    RAISE EXCEPTION 'workflow_run % not found or not owned by caller', p_run_id
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Serialize per-run event id allocation.
  PERFORM pg_advisory_xact_lock(hashtext(p_run_id::text));

  SELECT COALESCE(MAX(e.event_id), 0) + 1
    INTO v_next_id
  FROM lenses.workflow_run_events e
  WHERE e.run_id = p_run_id;

  INSERT INTO lenses.workflow_run_events (run_id, event_id, type, payload)
  VALUES (p_run_id, v_next_id, p_type, COALESCE(p_payload, '{}'::jsonb))
  RETURNING workflow_run_events.created_at INTO v_created_at;

  RETURN QUERY SELECT v_next_id, v_created_at;
END;
$$;


ALTER FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") IS 'Appends a workflow run event with monotonic per-run event_id. Owner-gated.';



CREATE OR REPLACE FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'media', 'public'
    AS $$
BEGIN
  UPDATE lenses.workflow_runs
  SET media_manifest = media_manifest || jsonb_build_object(
    'object_id',  p_object_id,
    'media_type', p_media_type,
    'mime_type',  p_mime_type,
    'node_id',    p_node_id,
    'added_at',   now()
  )
  WHERE id = p_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") IS 'AP: Appends a media object entry to lenses.workflow_runs.media_manifest. Called by the WorkflowExecutionService after fn_complete_async_run succeeds. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_inv agents.tool_invocations;
BEGIN
  SELECT * INTO v_inv FROM agents.tool_invocations WHERE id = p_invocation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'invocation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_inv.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF v_inv.approval_status <> 'pending' THEN
    RAISE EXCEPTION 'invocation is not pending approval' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.tool_invocations
     SET approval_status     = 'approved',
         approval_decided_by = auth.uid(),
         status              = 'running',
         started_at          = now()
   WHERE id = p_invocation_id;
END;
$$;


ALTER FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_archive('agent', p_ai_lenser_id); $$;


ALTER FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_archive('lens', p_lens_id); $$;


ALTER FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_archive('workflow', p_workflow_id); $$;


ALTER FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type  TEXT := lower(trim(p_artifact_type));
  v_actor UUID := lensers.get_auth_lenser_id();
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  IF v_type = 'lens' THEN
    UPDATE lenses.lenses
       SET archived_at = COALESCE(archived_at, now()),
           status = 'archived'::content.content_status,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    UPDATE lenses.workflows
       SET archived_at = COALESCE(archived_at, now()),
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    UPDATE battles.battles
       SET archived_at = COALESCE(archived_at, now()),
           status = 'archived'::battles.battle_status_enum,
           updated_at = now()
     WHERE id = p_artifact_id;
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (p_artifact_id, 'archived', v_actor, jsonb_build_object('action', 'archived'));
  ELSIF v_type = 'agent' THEN
    UPDATE agents.ai_lensers
       SET archived_at = COALESCE(archived_at, now()),
           is_active = false,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  PERFORM public.fn_log_artifact_lifecycle_event(
    v_type,
    p_artifact_id,
    'archive',
    v_actor,
    public.fn_artifact_dependency_summary(v_type, p_artifact_id)
  );

  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id);
END;
$$;


ALTER FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers'
    AS $$
DECLARE
  v_type  TEXT := lower(trim(p_artifact_type));
  v_actor UUID := lensers.get_auth_lenser_id();
BEGIN
  IF public.fn_lifecycle_is_service_role() THEN
    RETURN TRUE;
  END IF;

  IF v_actor IS NULL THEN
    RETURN FALSE;
  END IF;

  IF v_type IN ('lens', 'lenses') THEN
    RETURN EXISTS (
      SELECT 1 FROM lenses.lenses l
      WHERE l.id = p_artifact_id AND l.lenser_id = v_actor
    );
  ELSIF v_type IN ('workflow', 'workflows') THEN
    RETURN EXISTS (
      SELECT 1 FROM lenses.workflows w
      WHERE w.id = p_artifact_id AND w.lenser_id = v_actor
    );
  ELSIF v_type IN ('battle', 'battles') THEN
    RETURN EXISTS (
      SELECT 1 FROM battles.battles b
      WHERE b.id = p_artifact_id AND b.creator_lenser_id = v_actor
    );
  ELSIF v_type IN ('agent', 'ai_lenser', 'ai_lenser_id', 'lenser') THEN
    RETURN agents.can_manage_ai_lenser(p_artifact_id);
  END IF;

  RETURN FALSE;
END;
$$;


ALTER FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type TEXT := lower(trim(p_artifact_type));
BEGIN
  IF public.fn_lifecycle_is_service_role()
     OR public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RETURN TRUE;
  END IF;

  IF v_type IN ('lens', 'lenses') THEN
    RETURN EXISTS (
      SELECT 1
      FROM lenses.lenses l
      JOIN lensers.profiles p ON p.id = l.lenser_id
      WHERE l.id = p_artifact_id
        AND l.visibility IN ('public'::content.visibility_enum, 'community'::content.visibility_enum)
        AND l.status = 'published'::content.content_status
        AND l.deleted_at IS NULL
        AND p.status = 'active'::lensers.lenser_status
    );
  ELSIF v_type IN ('workflow', 'workflows') THEN
    RETURN EXISTS (
      SELECT 1
      FROM lenses.workflows w
      JOIN lensers.profiles p ON p.id = w.lenser_id
      WHERE w.id = p_artifact_id
        AND w.visibility = 'public'
        AND w.deleted_at IS NULL
        AND p.status = 'active'::lensers.lenser_status
    );
  ELSIF v_type IN ('battle', 'battles') THEN
    RETURN EXISTS (
      SELECT 1
      FROM battles.battles b
      WHERE b.id = p_artifact_id
        AND b.deleted_at IS NULL
        AND b.status IN (
          'open'::battles.battle_status_enum,
          'executing'::battles.battle_status_enum,
          'voting'::battles.battle_status_enum,
          'scoring'::battles.battle_status_enum,
          'closed'::battles.battle_status_enum,
          'published'::battles.battle_status_enum,
          'archived'::battles.battle_status_enum
        )
    );
  ELSIF v_type IN ('agent', 'ai_lenser', 'ai_lenser_id', 'lenser') THEN
    RETURN EXISTS (
      SELECT 1
      FROM agents.ai_lensers al
      WHERE al.id = p_artifact_id
        AND al.is_active = TRUE
        AND al.suspended_at IS NULL
        AND al.deleted_at IS NULL
    );
  END IF;

  RETURN FALSE;
END;
$$;


ALTER FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type       TEXT := lower(trim(p_artifact_type));
  v_actor      UUID := lensers.get_auth_lenser_id();
  v_dependency JSONB;
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_dependency := public.fn_artifact_dependency_summary(v_type, p_artifact_id);

  IF v_type = 'lens' THEN
    UPDATE lenses.lenses
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           status = 'archived'::content.content_status,
           visibility = 'private'::content.visibility_enum,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    UPDATE lenses.workflows
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           visibility = 'private',
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    UPDATE battles.battles
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           status = 'archived'::battles.battle_status_enum,
           updated_at = now()
     WHERE id = p_artifact_id;
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
      p_artifact_id,
      'archived',
      v_actor,
      jsonb_build_object('action', 'delete_tombstone', 'dependency_summary', v_dependency)
    );
  ELSIF v_type = 'agent' THEN
    UPDATE agents.ai_lensers
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           is_active = false,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  PERFORM public.fn_log_artifact_lifecycle_event(
    v_type,
    p_artifact_id,
    'delete_tombstone',
    v_actor,
    v_dependency
  );

  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id)
    || jsonb_build_object('delete_mode', 'tombstone');
END;
$$;


ALTER FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") IS 'Dependency-aware delete request. Public APIs tombstone/withdraw governed artifacts instead of hard-deleting competitive or historical evidence.';



CREATE OR REPLACE FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'execution', 'content'
    AS $$
DECLARE
  v_type    TEXT := lower(trim(p_artifact_type));
  v_counts  JSONB := '{}'::jsonb;
  v_total   INTEGER := 0;
  v_count   INTEGER := 0;
  v_reasons TEXT[] := '{}';
BEGIN
  IF v_type IN ('lens', 'lenses') THEN
    SELECT count(*)::int INTO v_count
    FROM execution.requests r
    WHERE r.lens_id = p_artifact_id
       OR r.version_id IN (SELECT v.id FROM lenses.versions v WHERE v.lens_id = p_artifact_id);
    v_counts := v_counts || jsonb_build_object('executions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s execution requests', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.workflow_nodes WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('workflow_nodes', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s workflow nodes', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.workflow_version_nodes WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('workflow_version_nodes', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s workflow version nodes', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.battles WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battles', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battles', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.contender_lens_assignments WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battle_lens_assignments', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battle lens assignments', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.generated_challenges
    WHERE generator_lens_id = p_artifact_id
       OR generator_version_id IN (SELECT v.id FROM lenses.versions v WHERE v.lens_id = p_artifact_id);
    v_counts := v_counts || jsonb_build_object('generated_challenges', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s generated challenges', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.lens_bindings WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('agent_lens_bindings', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s agent lens bindings', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.lenses WHERE parent_lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('forks', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s forks', v_count); END IF;

    SELECT count(*)::int INTO v_count
    FROM lenses.versions
    WHERE lens_id = p_artifact_id AND status = 'published'::content.content_status;
    v_counts := v_counts || jsonb_build_object('published_versions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s published versions', v_count); END IF;

  ELSIF v_type IN ('workflow', 'workflows') THEN
    SELECT count(*)::int INTO v_count FROM lenses.workflow_runs WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('workflow_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s workflow runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.battles WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battles', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battles', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.submissions WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battle_submissions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battle submissions', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.team_runs WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('agent_team_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s agent team runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.workflows WHERE parent_workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('forks', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s forks', v_count); END IF;

  ELSIF v_type IN ('battle', 'battles') THEN
    SELECT count(*)::int INTO v_count FROM battles.submissions WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('submissions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s submissions', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.votes WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('votes', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s votes', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.contender_runs WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('contender_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s contender runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.battle_execution_jobs WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('execution_jobs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s execution jobs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.rule_snapshots WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('rule_snapshots', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s rule snapshots', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.generated_challenges WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('generated_challenges', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s generated challenges', v_count); END IF;

    SELECT count(*)::int INTO v_count
    FROM battles.battles
    WHERE id = p_artifact_id AND status <> 'draft'::battles.battle_status_enum;
    v_counts := v_counts || jsonb_build_object('started_or_public_state', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || 'battle has started or been published'::text; END IF;

  ELSIF v_type IN ('agent', 'ai_lenser', 'ai_lenser_id', 'lenser') THEN
    SELECT count(*)::int INTO v_count FROM battles.contender_entity_map WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battle_contenders', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battle contender slots', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.team_runs WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('team_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s team runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.run_reports WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('run_reports', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s run reports', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.policy_evaluations WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('policy_evaluations', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s policy evaluations', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.action_logs WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('action_logs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s action logs', v_count); END IF;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  RETURN jsonb_build_object(
    'artifact_type', v_type,
    'artifact_id', p_artifact_id,
    'counts', v_counts,
    'total', v_total,
    'blocking_reasons', to_jsonb(v_reasons),
    'has_dependencies', v_total > 0,
    'can_hard_delete', v_total = 0
  );
END;
$$;


ALTER FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") IS 'Lifecycle governance dependency detector for Lens, Workflow, Battle, and Agent. Used to block unsafe hard delete/mutation and explain archive/tombstone fallback.';



CREATE OR REPLACE FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type       TEXT := lower(trim(p_artifact_type));
  v_actor      UUID := lensers.get_auth_lenser_id();
  v_dependency JSONB;
  v_pinned     BOOLEAN := FALSE;
  v_status     JSONB;
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_view(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_dependency := public.fn_artifact_dependency_summary(v_type, p_artifact_id);

  IF v_actor IS NOT NULL THEN
    SELECT EXISTS (
      SELECT 1
      FROM content.reactions r
      WHERE r.entity_type = v_type::content.entity_type_enum
        AND r.entity_id = p_artifact_id
        AND r.lenser_id = v_actor
        AND r.reaction = 'saved'::content.reaction_enum
    ) INTO v_pinned;
  END IF;

  IF v_type = 'lens' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'lens',
      'artifact_id', l.id,
      'state', CASE
        WHEN l.deleted_at IS NOT NULL THEN 'deleted'
        WHEN l.archived_at IS NOT NULL OR l.status = 'archived'::content.content_status THEN 'archived'
        ELSE l.status::text
      END,
      'visibility', l.visibility::text,
      'archived_at', l.archived_at,
      'deleted_at', l.deleted_at,
      'pinned', v_pinned,
      'version_id', l.head_version_id,
      'snapshot_hash', encode(v.content_hash, 'hex'),
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM lenses.lenses l
    LEFT JOIN lenses.versions v ON v.id = l.head_version_id
    WHERE l.id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'workflow',
      'artifact_id', w.id,
      'state', CASE
        WHEN w.deleted_at IS NOT NULL THEN 'deleted'
        WHEN w.archived_at IS NOT NULL THEN 'archived'
        ELSE 'active'
      END,
      'visibility', w.visibility,
      'archived_at', w.archived_at,
      'deleted_at', w.deleted_at,
      'pinned', v_pinned,
      'version_id', w.head_version_id,
      'snapshot_hash', wv.snapshot_hash,
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM lenses.workflows w
    LEFT JOIN lenses.workflow_versions wv ON wv.id = w.head_version_id
    WHERE w.id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'battle',
      'artifact_id', b.id,
      'state', CASE
        WHEN b.deleted_at IS NOT NULL THEN 'deleted'
        WHEN b.archived_at IS NOT NULL OR b.status = 'archived'::battles.battle_status_enum THEN 'archived'
        ELSE b.status::text
      END,
      'visibility', CASE WHEN b.deleted_at IS NULL THEN 'historical' ELSE 'tombstone' END,
      'archived_at', b.archived_at,
      'deleted_at', b.deleted_at,
      'pinned', v_pinned,
      'version_id', NULL,
      'snapshot_hash', rs.snapshot_hash,
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM battles.battles b
    LEFT JOIN LATERAL (
      SELECT snapshot_hash
      FROM battles.rule_snapshots
      WHERE battle_id = b.id
      ORDER BY created_at DESC
      LIMIT 1
    ) rs ON TRUE
    WHERE b.id = p_artifact_id;
  ELSIF v_type = 'agent' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'agent',
      'artifact_id', al.id,
      'state', CASE
        WHEN al.deleted_at IS NOT NULL THEN 'deleted'
        WHEN al.archived_at IS NOT NULL THEN 'archived'
        WHEN al.is_active AND al.suspended_at IS NULL THEN 'active'
        WHEN al.suspended_at IS NOT NULL THEN 'suspended'
        ELSE 'inactive'
      END,
      'visibility', CASE WHEN al.is_active AND al.suspended_at IS NULL THEN 'public' ELSE 'restricted' END,
      'archived_at', al.archived_at,
      'deleted_at', al.deleted_at,
      'pinned', v_pinned,
      'version_id', NULL,
      'snapshot_hash', NULL,
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM agents.ai_lensers al
    WHERE al.id = p_artifact_id;
  END IF;

  IF v_status IS NULL THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  RETURN v_status;
END;
$$;


ALTER FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") IS 'Returns lifecycle state, pin state, version/snapshot ids, and dependency summary for a governed artifact.';



CREATE OR REPLACE FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean DEFAULT true) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
DECLARE
  v_actor UUID := lensers.get_auth_lenser_id();
  v_type  TEXT := lower(trim(p_artifact_type));
  v_entity content.entity_type_enum;
BEGIN
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
  END IF;

  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF v_type NOT IN ('lens', 'workflow', 'battle', 'agent') THEN
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  IF NOT public.fn_artifact_can_view(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_entity := v_type::content.entity_type_enum;

  IF p_pin THEN
    INSERT INTO content.reactions (entity_type, entity_id, lenser_id, reaction)
    VALUES (v_entity, p_artifact_id, v_actor, 'saved'::content.reaction_enum)
    ON CONFLICT (entity_type, entity_id, lenser_id, reaction) DO NOTHING;
    PERFORM public.fn_log_artifact_lifecycle_event(v_type, p_artifact_id, 'pin', v_actor, '{}'::jsonb);
  ELSE
    DELETE FROM content.reactions
    WHERE entity_type = v_entity
      AND entity_id = p_artifact_id
      AND lenser_id = v_actor
      AND reaction = 'saved'::content.reaction_enum;
    PERFORM public.fn_log_artifact_lifecycle_event(v_type, p_artifact_id, 'unpin', v_actor, '{}'::jsonb);
  END IF;

  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id);
END;
$$;


ALTER FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type  TEXT := lower(trim(p_artifact_type));
  v_actor UUID := lensers.get_auth_lenser_id();
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  IF v_type = 'lens' THEN
    UPDATE lenses.lenses
       SET archived_at = NULL,
           deleted_at = NULL,
           status = CASE
             WHEN EXISTS (
               SELECT 1 FROM lenses.versions v
               WHERE v.lens_id = p_artifact_id
                 AND v.status = 'published'::content.content_status
             ) THEN 'published'::content.content_status
             ELSE 'draft'::content.content_status
           END,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    UPDATE lenses.workflows
       SET archived_at = NULL,
           deleted_at = NULL,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    -- Restore to the most appropriate pre-archive status. Never auto-promote
    -- to 'open' — a battle that was never published (published_at IS NULL and
    -- finalized_at IS NULL) was a draft and must return to draft.
    UPDATE battles.battles
       SET archived_at = NULL,
           deleted_at = NULL,
           status = CASE
             WHEN published_at IS NOT NULL THEN 'published'::battles.battle_status_enum
             WHEN finalized_at IS NOT NULL THEN 'closed'::battles.battle_status_enum
             ELSE 'draft'::battles.battle_status_enum
           END,
           updated_at = now()
     WHERE id = p_artifact_id
       AND status = 'archived'::battles.battle_status_enum;
  ELSIF v_type = 'agent' THEN
    -- Do NOT reactivate suspended agents. A suspended agent must be explicitly
    -- unsuspended through a separate governance action. Archiving + restoring
    -- must not be a suspension bypass vector.
    UPDATE agents.ai_lensers
       SET archived_at = NULL,
           deleted_at = NULL,
           is_active = CASE WHEN suspended_at IS NULL THEN true ELSE false END,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  PERFORM public.fn_log_artifact_lifecycle_event(v_type, p_artifact_id, 'restore', v_actor, '{}'::jsonb);
  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id);
END;
$$;


ALTER FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM agents.policies ap
    WHERE ap.ai_lenser_id = p_agent_id
      AND p_modality = ANY(ap.allowed_output_modalities)
  ) THEN
    RAISE EXCEPTION 'modality_not_allowed: agent % cannot produce % output. '
      'Update agents.policies.allowed_output_modalities to unlock.', p_agent_id, p_modality
    USING ERRCODE = 'P0001';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") IS 'AP: Raises modality_not_allowed if p_modality is not in agents.policies.allowed_output_modalities for p_agent_id. Called by WorkflowExecutionService before dispatching any non-text generative node. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_input_snapshot" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "battle_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "assigned_at" timestamp with time zone, "input_snapshot" "jsonb")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  INSERT INTO battles.contender_lens_assignments
    (contender_id, battle_id, lens_id, version_id, input_snapshot)
  VALUES
    (p_contender_id, p_battle_id, p_lens_id, p_version_id, p_input_snapshot)
  ON CONFLICT (contender_id) DO UPDATE
    SET lens_id        = EXCLUDED.lens_id,
        version_id     = EXCLUDED.version_id,
        input_snapshot = EXCLUDED.input_snapshot
  RETURNING id, contender_id, battle_id, lens_id, version_id, assigned_at, input_snapshot;
$$;


ALTER FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."tool_assignments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "tool_id" "uuid" NOT NULL,
    "profile_id" "uuid",
    "allowed" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."tool_assignments" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid" DEFAULT NULL::"uuid", "p_allowed" boolean DEFAULT true) RETURNS "agents"."tool_assignments"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.tool_assignments;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.tool_assignments (ai_lenser_id, tool_id, profile_id, allowed)
  VALUES (p_ai_lenser_id, p_tool_id, p_profile_id, COALESCE(p_allowed, true))
  ON CONFLICT (ai_lenser_id, tool_id)
  DO UPDATE SET
    profile_id = EXCLUDED.profile_id,
    allowed    = EXCLUDED.allowed
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_approve_device_request(p_user_code => p_user_code);
END;
$$;


ALTER FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_exchange_device_approval(
    p_request_id => p_request_id,
    p_request_secret => p_request_secret
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_exchange_device_login(
    p_request_id     => p_request_id,
    p_request_secret => p_request_secret
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_list_developer_tokens"() RETURNS TABLE("id" "uuid", "label" "text", "tokenPrefix" "text", "status" "text", "expiresAt" timestamp with time zone, "createdAt" timestamp with time zone, "revokedAt" timestamp with time zone, "lastUsedAt" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM authz.fn_list_developer_tokens();
END;
$$;


ALTER FUNCTION "public"."fn_auth_list_developer_tokens"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text" DEFAULT NULL::"text", "p_request_ttl_minutes" integer DEFAULT 10, "p_token_ttl_hours" integer DEFAULT 24) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_request_device_approval(
    p_label => p_label,
    p_request_ttl_minutes => p_request_ttl_minutes,
    p_token_ttl_hours => p_token_ttl_hours
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer DEFAULT 10) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_request_device_login(
    p_request_ttl_minutes => p_request_ttl_minutes
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  PERFORM authz.fn_revoke_developer_token(p_token_id => p_token_id);
END;
$$;


ALTER FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_store_device_login_session(
    p_user_code     => p_user_code,
    p_access_token  => p_access_token,
    p_refresh_token => p_refresh_token
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean DEFAULT false) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'agents'
    AS $_$
DECLARE
  v_caller          uuid := lensers.get_auth_lenser_id();
  v_schedule        record;
  v_parts           text[];
  v_minute          text;
  v_hour            text;
  v_dom             text;
  v_mon             text;
  v_dow             text;
  v_step_minutes    int;
  v_target_hour     int;
  v_target_dow      int;
  v_tick            timestamptz;
  v_dispatched      int := 0;
  v_skipped         int := 0;
  v_ticks           jsonb := '[]'::jsonb;
  v_backfill_key    text;
  v_run_id          uuid;
  v_now             timestamptz := date_trunc('minute', now());
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  IF p_since IS NULL OR p_since >= now() THEN
    RAISE EXCEPTION 'p_since must be in the past' USING ERRCODE = '22023';
  END IF;

  -- Cap window at 30 days to bound cost.
  IF p_since < now() - interval '30 days' THEN
    RAISE EXCEPTION 'p_since exceeds 30-day backfill window' USING ERRCODE = '22023';
  END IF;

  SELECT s.id,
         s.workflow_id,
         s.cron_expr,
         s.inputs_template,
         s.global_model_id,
         w.lenser_id   AS owner_id
  INTO   v_schedule
  FROM   lenses.workflow_schedules s
  JOIN   lenses.workflows w ON w.id = s.workflow_id
  WHERE  s.id = p_schedule_id;

  IF v_schedule.id IS NULL THEN
    RAISE EXCEPTION 'schedule_not_found: %', p_schedule_id USING ERRCODE = 'P0002';
  END IF;

  IF v_schedule.owner_id IS DISTINCT FROM v_caller THEN
    RAISE EXCEPTION 'not_workflow_owner' USING ERRCODE = '42501';
  END IF;

  -- ── Parse the cron expression ──────────────────────────────────────────
  v_parts := regexp_split_to_array(trim(v_schedule.cron_expr), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RAISE EXCEPTION
      'unsupported_cron_expression: % (expected 5 fields)',
      v_schedule.cron_expr
      USING ERRCODE = '22023';
  END IF;

  v_minute := v_parts[1];
  v_hour   := v_parts[2];
  v_dom    := v_parts[3];
  v_mon    := v_parts[4];
  v_dow    := v_parts[5];

  -- Build the tick stream. Truncate p_since to minute precision.
  IF v_minute = '*' AND v_hour = '*' AND v_dom = '*' AND v_mon = '*' AND v_dow = '*' THEN
    -- every minute
    FOR v_tick IN
      SELECT g
      FROM generate_series(date_trunc('minute', p_since), v_now, interval '1 minute') g
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSIF v_minute LIKE '*/%' AND v_hour = '*' AND v_dom = '*' AND v_mon = '*' AND v_dow = '*' THEN
    -- every N minutes
    BEGIN
      v_step_minutes := substr(v_minute, 3)::int;
    EXCEPTION WHEN OTHERS THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END;

    IF v_step_minutes <= 0 OR v_step_minutes > 59 THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END IF;

    FOR v_tick IN
      SELECT g
      FROM generate_series(date_trunc('hour', p_since), v_now, (v_step_minutes::text || ' minutes')::interval) g
      WHERE g >= p_since
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSIF v_minute = '0' AND v_dom = '*' AND v_mon = '*' AND v_dow = '*'
        AND v_hour ~ '^[0-9]+$' THEN
    -- 0 H * * *  (daily at hour H, UTC)
    v_target_hour := v_hour::int;
    IF v_target_hour < 0 OR v_target_hour > 23 THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END IF;

    FOR v_tick IN
      SELECT g
      FROM generate_series(
        date_trunc('day', p_since AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
          + (v_target_hour::text || ' hours')::interval,
        v_now,
        interval '1 day'
      ) g
      WHERE g >= p_since AND g <= v_now
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSIF v_minute = '0' AND v_dom = '*' AND v_mon = '*'
        AND v_hour ~ '^[0-9]+$' AND v_dow ~ '^[0-6]$' THEN
    -- 0 H * * DOW  (weekly, hour H, day-of-week DOW; 0=Sun)
    v_target_hour := v_hour::int;
    v_target_dow  := v_dow::int;
    IF v_target_hour < 0 OR v_target_hour > 23 THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END IF;

    FOR v_tick IN
      SELECT g
      FROM generate_series(
        date_trunc('day', p_since AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
          + (v_target_hour::text || ' hours')::interval,
        v_now,
        interval '1 day'
      ) g
      WHERE g >= p_since AND g <= v_now
        AND extract(dow from g)::int = v_target_dow
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSE
    RAISE EXCEPTION
      'unsupported_cron_expression: % (supported: ''*/N * * * *'', ''0 H * * *'', ''0 H * * DOW'')',
      v_schedule.cron_expr
      USING ERRCODE = '22023';
  END IF;

  -- ── Dry-run path ───────────────────────────────────────────────────────
  IF p_dry_run THEN
    RETURN jsonb_build_object(
      'would_dispatch', jsonb_array_length(v_ticks),
      'ticks',          v_ticks
    );
  END IF;

  -- ── Execute path ───────────────────────────────────────────────────────
  FOR v_tick IN
    SELECT (value::text)::timestamptz FROM jsonb_array_elements_text(v_ticks)
  LOOP
    v_backfill_key := p_schedule_id::text || ':' ||
                      to_char(v_tick AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"');

    -- Skip if a run for this exact slot already exists (either via
    -- backfill_id metadata key or via the existing schedule_id+scheduled_for
    -- unique slot index).
    IF EXISTS (
      SELECT 1 FROM lenses.workflow_runs
      WHERE metadata->>'backfill_id' = v_backfill_key
    ) OR EXISTS (
      SELECT 1 FROM lenses.workflow_runs
      WHERE schedule_id = p_schedule_id
        AND scheduled_for = v_tick
    ) THEN
      v_skipped := v_skipped + 1;
      CONTINUE;
    END IF;

    INSERT INTO lenses.workflow_runs (
      workflow_id,
      triggered_by,
      status,
      context_inputs,
      global_model_id,
      schedule_id,
      scheduled_for,
      trigger_mode,
      metadata
    ) VALUES (
      v_schedule.workflow_id,
      v_caller,
      'pending',
      COALESCE(v_schedule.inputs_template, '{}'::jsonb),
      v_schedule.global_model_id,
      p_schedule_id,
      v_tick,
      'schedule',
      jsonb_build_object('backfill_id', v_backfill_key, 'origin', 'backfill')
    )
    RETURNING id INTO v_run_id;

    -- Materialise node placeholders so the worker can pick this run up.
    INSERT INTO lenses.workflow_node_results (run_id, node_id, status)
    SELECT v_run_id, n.id, 'pending'
    FROM   lenses.workflow_nodes n
    WHERE  n.workflow_id = v_schedule.workflow_id;

    v_dispatched := v_dispatched + 1;
  END LOOP;

  RETURN jsonb_build_object(
    'dispatched',       v_dispatched,
    'skipped_existing', v_skipped
  );
END;
$_$;


ALTER FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) IS 'Phase Q1: enumerates missed cron ticks between p_since and now() for a workflow schedule the caller owns and inserts pending workflow_runs the recovery sweeper will pick up. Supports * * * * *, */N * * * *, 0 H * * *, 0 H * * DOW. Dedupes via metadata.backfill_id and (schedule_id, scheduled_for). Returns jsonb with dispatched/skipped or would_dispatch/ticks.';



CREATE OR REPLACE FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'xp', 'public'
    AS $$
DECLARE
  v_total_xp   BIGINT;
  v_level      INT;
  v_streak     INT;
  v_forum_app  CONSTANT uuid := '00000000-0000-0000-0000-000000000001';
BEGIN
  -- Fetch current XP + level
  SELECT total_xp, current_level
    INTO v_total_xp, v_level
    FROM xp.totals
   WHERE lenser_id = p_lenser_id
     AND app_id    = v_forum_app;

  IF NOT FOUND THEN
    RETURN;
  END IF;

  -- Fetch daily streak
  SELECT current_streak
    INTO v_streak
    FROM xp.streaks
   WHERE lenser_id   = p_lenser_id
     AND streak_type = 'daily';

  -- ---- Level milestone badges -------------------------------------------
  -- Newcomer  (level 5)
  IF v_level >= 5 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_5', 'Newcomer', 'Reached Level 5', '🌱')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Builder (level 10)
  IF v_level >= 10 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_10', 'Builder', 'Reached Level 10', '🏗️')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Expert (level 25)
  IF v_level >= 25 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_25', 'Expert', 'Reached Level 25', '⚡')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Architect (level 50)
  IF v_level >= 50 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_50', 'Architect', 'Reached Level 50', '🏛️')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Legend (level 75)
  IF v_level >= 75 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_75', 'Legend', 'Reached Level 75', '🌟')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Champion (level 100)
  IF v_level >= 100 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_100', 'Champion', 'Reached Level 100 — all-time elite', '🏆')
    ON CONFLICT DO NOTHING;
  END IF;

  -- ---- XP milestone badges -----------------------------------------------
  IF v_total_xp >= 1000 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'xp_1k', '1K XP', 'Earned 1,000 total XP', '💡')
    ON CONFLICT DO NOTHING;
  END IF;

  IF v_total_xp >= 10000 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'xp_10k', '10K XP', 'Earned 10,000 total XP', '🔥')
    ON CONFLICT DO NOTHING;
  END IF;

  IF v_total_xp >= 50000 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'xp_50k', '50K XP', 'Earned 50,000 total XP', '💎')
    ON CONFLICT DO NOTHING;
  END IF;

  -- ---- Streak milestone badges -------------------------------------------
  IF v_streak >= 7 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'streak_7d', '7-Day Streak', 'Logged in 7 days in a row', '🔆')
    ON CONFLICT DO NOTHING;
  END IF;

  IF v_streak >= 30 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'streak_30d', '30-Day Streak', 'Logged in 30 days in a row', '☀️')
    ON CONFLICT DO NOTHING;
  END IF;

EXCEPTION WHEN OTHERS THEN
  -- Non-fatal: badge award errors must not roll back the parent XP transaction
  RAISE NOTICE 'fn_badge_check_and_award: % (lenser=%)', SQLERRM, p_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") IS 'CB: Awards milestone badges based on XP totals, level, and streak. SECURITY DEFINER. Non-fatal on error.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_creator_id uuid;
BEGIN
  SELECT id INTO v_creator_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_creator_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated.';
  END IF;

  UPDATE battles.battles
  SET
    status           = 'scoring',
    voting_closes_at = now(),
    updated_at       = now()
  WHERE id                = p_battle_id
    AND creator_lenser_id = v_creator_id
    AND status            = 'voting'
    AND deleted_at        IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Battle not found, not owned by you, or not in voting status.';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") IS 'Transitions a battle from voting to scoring. Sets voting_closes_at = now(). Only callable by the battle creator. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'execution', 'public'
    AS $$
DECLARE
  v_lenser_id  UUID;
  v_battle     battles.battles%ROWTYPE;
  v_contender  battles.contenders%ROWTYPE;
BEGIN
  -- Resolve caller lenser
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  -- Verify agent ownership
  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    WHERE  al.id = p_agent_id AND al.profile_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'dispatch_agent_forbidden: agent not owned by caller';
  END IF;

  -- Verify battle is in a submittable state
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'dispatch_agent_battle_not_found: %', p_battle_id;
  END IF;
  IF v_battle.status NOT IN ('open', 'executing') THEN
    RAISE EXCEPTION 'dispatch_agent_battle_not_open: status=%', v_battle.status;
  END IF;

  -- Verify contender registration
  SELECT c.* INTO v_contender
  FROM   battles.contenders c
  JOIN   lensers.profiles p ON p.id = c.lenser_id
  WHERE  c.battle_id = p_battle_id
  AND    p.id = v_lenser_id
  LIMIT  1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'dispatch_agent_not_registered: join the battle first';
  END IF;

  -- Log dispatch intent
  INSERT INTO audit.events (event_type, actor_type, actor_id, payload)
  VALUES (
    'battle.agent_dispatch_queued',
    'lenser',
    v_lenser_id,
    jsonb_build_object(
      'battle_id',   p_battle_id,
      'agent_id',    p_agent_id,
      'model_spec',  p_model_spec,
      'device_id',   p_device_id,
      'workflow_id', p_workflow_id
    )
  );

  RETURN jsonb_build_object(
    'battle_id',    p_battle_id,
    'agent_id',     p_agent_id,
    'contender_id', v_contender.id,
    'model_spec',   p_model_spec,
    'status',       'queued'
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'audit', 'public'
    AS $$
DECLARE
  v_operator_id UUID;
  v_old_status  TEXT;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'force_transition_forbidden: admin role required';
  END IF;

  SELECT p.id INTO v_operator_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  SELECT status::TEXT INTO v_old_status
  FROM   battles.battles
  WHERE  id = p_battle_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'force_transition_battle_not_found: %', p_battle_id;
  END IF;

  IF p_target_status NOT IN ('draft', 'open', 'executing', 'voting', 'scoring', 'closed', 'published', 'archived') THEN
    RAISE EXCEPTION 'force_transition_invalid_status: %', p_target_status;
  END IF;

  UPDATE battles.battles
  SET    status       = p_target_status::battles.battle_status_enum,
         updated_at   = now(),
         published_at = CASE WHEN p_target_status = 'published' THEN now() ELSE published_at END,
         finalized_at = CASE WHEN p_target_status IN ('closed', 'published') THEN now() ELSE finalized_at END
  WHERE  id = p_battle_id;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'battle.admin.force_transition',
    'lenser',
    v_operator_id,
    'warn',
    jsonb_build_object(
      'battle_id',     p_battle_id,
      'from_status',   v_old_status,
      'to_status',     p_target_status,
      'reason',        p_reason
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean DEFAULT false) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_link  battles.invite_links%ROWTYPE;
BEGIN
  SELECT * INTO v_link
    FROM battles.invite_links
   WHERE token = p_token
   FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'invite_token_not_found' USING ERRCODE = 'P0001';
  END IF;

  IF v_link.expires_at IS NOT NULL AND v_link.expires_at < NOW() THEN
    RAISE EXCEPTION 'invite_token_expired' USING ERRCODE = 'P0001';
  END IF;

  -- Increment the right counter.
  IF p_via_qr OR v_link.type = 'qr' THEN
    UPDATE battles.invite_links
       SET qr_scan_count = qr_scan_count + 1,
           accepted_at   = COALESCE(accepted_at, NOW())
     WHERE id = v_link.id;
  ELSE
    UPDATE battles.invite_links
       SET click_count  = click_count + 1,
           accepted_at  = COALESCE(accepted_at, NOW())
     WHERE id = v_link.id;
  END IF;

  RETURN jsonb_build_object(
    'battle_id',  v_link.battle_id,
    'type',       v_link.type,
    'invite_url', v_link.invite_url
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) IS 'Records a click or QR scan on a battle invite token. Granted to anon so it works before the visitor has an account.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text" DEFAULT 'public'::"text", "p_target_handle" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller      UUID;
  v_link_type   battles.invite_link_type;
  v_token       TEXT;
  v_url         TEXT;
  v_id          UUID;
  v_base_url    TEXT := 'https://lenserfight.com';
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  -- Validate caller owns the battle.
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
     WHERE b.id = p_battle_id
       AND b.creator_lenser_id = v_caller
  ) THEN
    RAISE EXCEPTION 'battle_not_found_or_not_owner' USING ERRCODE = 'P0001';
  END IF;

  -- Validate type.
  BEGIN
    v_link_type := p_type::battles.invite_link_type;
  EXCEPTION WHEN invalid_text_representation THEN
    RAISE EXCEPTION 'invalid_invite_type: %', p_type USING ERRCODE = 'P0001';
  END;

  -- Private requires target_handle.
  IF v_link_type = 'private' AND p_target_handle IS NULL THEN
    RAISE EXCEPTION 'private_invite_requires_target_handle' USING ERRCODE = 'P0001';
  END IF;

  -- Generate token and URL.
  v_token := encode(extensions.gen_random_bytes(10), 'hex');

  -- Build battle slug from battle id (slug stored on battle or fallback to id).
  SELECT COALESCE(
    (SELECT b.slug FROM battles.battles b WHERE b.id = p_battle_id),
    p_battle_id::TEXT
  ) INTO v_url;
  v_url := v_base_url || '/b/' || v_url || '?ref=' || v_token;

  INSERT INTO battles.invite_links (
    battle_id, created_by, type, token, invite_url, target_handle
  ) VALUES (
    p_battle_id, v_caller, v_link_type, v_token, v_url, p_target_handle
  )
  RETURNING id INTO v_id;

  RETURN jsonb_build_object(
    'id',         v_id,
    'token',      v_token,
    'invite_url', v_url,
    'type',       p_type,
    'battle_id',  p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") IS 'Creates a battle invite link (public/private/link/qr). Caller must own the battle. Returns id, token, and invite_url.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer DEFAULT 50) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller UUID;
  v_rows   JSONB;
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
     WHERE b.id = p_battle_id
       AND b.creator_lenser_id = v_caller
  ) THEN
    RAISE EXCEPTION 'battle_not_found_or_not_owner' USING ERRCODE = 'P0001';
  END IF;

  SELECT jsonb_agg(
    jsonb_build_object(
      'id',             il.id,
      'type',           il.type,
      'token',          il.token,
      'invite_url',     il.invite_url,
      'target_handle',  il.target_handle,
      'click_count',    il.click_count,
      'qr_scan_count',  il.qr_scan_count,
      'accepted_at',    il.accepted_at,
      'created_at',     il.created_at,
      'expires_at',     il.expires_at
    )
    ORDER BY il.created_at DESC
  )
  INTO v_rows
  FROM (
    SELECT * FROM battles.invite_links
     WHERE battle_id = p_battle_id
     LIMIT LEAST(p_limit, 200)
  ) il;

  RETURN COALESCE(v_rows, '[]'::JSONB);
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) IS 'Lists invite links for a battle (up to 200). Caller must own the battle.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller UUID;
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  -- Only the battle owner can view stats.
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
     WHERE b.id = p_battle_id
       AND b.creator_lenser_id = v_caller
  ) THEN
    RAISE EXCEPTION 'battle_not_found_or_not_owner' USING ERRCODE = 'P0001';
  END IF;

  RETURN (
    SELECT jsonb_build_object(
      'battle_id',    p_battle_id,
      'link_clicks',  COALESCE(SUM(click_count)   FILTER (WHERE type IN ('public','link')), 0),
      'qr_scans',     COALESCE(SUM(qr_scan_count) FILTER (WHERE type = 'qr'),              0),
      'accepted',     COUNT(*) FILTER (WHERE accepted_at IS NOT NULL),
      'total_links',  COUNT(*)
    )
    FROM battles.invite_links
   WHERE battle_id = p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") IS 'Returns aggregated click, scan, accepted, and total-link counts for a battle. Caller must own the battle.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_creator_id     uuid;
  v_accepted_count int;
BEGIN
  SELECT id INTO v_creator_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_creator_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated.';
  END IF;

  -- Require at least 2 accepted contenders before voting can open
  SELECT COUNT(*) INTO v_accepted_count
  FROM battles.contenders
  WHERE battle_id        = p_battle_id
    AND contender_status = 'accepted';

  IF v_accepted_count < 2 THEN
    RAISE EXCEPTION 'Battle must have at least 2 accepted contenders before voting can open.';
  END IF;

  UPDATE battles.battles
  SET
    status          = 'voting',
    voting_opens_at = now(),
    updated_at      = now()
  WHERE id                = p_battle_id
    AND creator_lenser_id = v_creator_id
    AND status            = 'open'
    AND deleted_at        IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Battle not found, not owned by you, or not in open status.';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") IS 'Transitions a battle from open to voting. Requires at least 2 accepted contenders. Sets voting_opens_at = now(). Only callable by the battle creator. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_judge_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_publish_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_auto_judge" boolean DEFAULT true, "p_auto_publish" boolean DEFAULT true) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_sched_id  UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid();

  IF NOT EXISTS (
    SELECT 1 FROM battles.battles
    WHERE id = p_battle_id AND creator_lenser_id = v_lenser_id
  ) AND NOT public.is_admin() THEN
    RAISE EXCEPTION 'set_schedule_forbidden: must be battle creator or admin';
  END IF;

  INSERT INTO battles.schedules (battle_id, open_at, judge_at, publish_at, auto_judge, auto_publish)
  VALUES (p_battle_id, p_open_at, p_judge_at, p_publish_at, p_auto_judge, p_auto_publish)
  ON CONFLICT (battle_id) DO UPDATE
    SET open_at      = EXCLUDED.open_at,
        judge_at     = EXCLUDED.judge_at,
        publish_at   = EXCLUDED.publish_at,
        auto_judge   = EXCLUDED.auto_judge,
        auto_publish = EXCLUDED.auto_publish,
        updated_at   = now()
  RETURNING id INTO v_sched_id;

  RETURN v_sched_id;
END;
$$;


ALTER FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid" DEFAULT NULL::"uuid", "p_agent_id" "uuid" DEFAULT NULL::"uuid", "p_content" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id    UUID;
  v_submission_id UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  -- Verify contender registration
  IF NOT EXISTS (
    SELECT 1 FROM battles.contenders c
    WHERE  c.battle_id  = p_battle_id
    AND    c.lenser_id  = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'submit_workflow_not_registered: join battle first';
  END IF;

  -- Verify workflow ownership or visibility
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE  w.id = p_workflow_id
    AND    (w.lenser_id = v_lenser_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'submit_workflow_not_accessible: workflow % not found or not accessible', p_workflow_id;
  END IF;

  -- Get contender_id
  INSERT INTO battles.submissions (
    battle_id,
    contender_id,
    submission_type,
    workflow_id,
    execution_run_id,
    source_type,
    content_text,
    status
  )
  SELECT
    p_battle_id,
    c.id,
    'workflow',
    p_workflow_id,
    p_run_id,
    CASE WHEN p_run_id IS NOT NULL THEN 'execution_output' ELSE 'manual' END,
    p_content,
    CASE WHEN p_content IS NOT NULL THEN 'submitted' ELSE 'pending' END
  FROM battles.contenders c
  WHERE c.battle_id = p_battle_id
  AND   c.lenser_id = v_lenser_id
  LIMIT 1
  RETURNING id INTO v_submission_id;

  RETURN v_submission_id;
END;
$$;


ALTER FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid" DEFAULT NULL::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  UPDATE battles.submissions s
  SET    content_text      = p_content,
         execution_run_id  = COALESCE(p_execution_run_id, s.execution_run_id),
         status            = 'submitted',
         submitted_at      = now(),
         updated_at        = now()
  FROM   battles.contenders c
  WHERE  s.id         = p_submission_id
  AND    c.id         = s.contender_id
  AND    c.lenser_id  = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'update_workflow_submission_forbidden_or_not_found: %', p_submission_id;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_archive('battle', p_battle_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  v_readiness JSONB;
  v_battle    RECORD;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id AND status = 'draft';
  IF NOT FOUND THEN
    RETURN FALSE;
  END IF;

  v_readiness := public.fn_battles_check_readiness(p_battle_id);

  IF NOT (v_readiness ->> 'ready')::BOOLEAN THEN
    RETURN FALSE;
  END IF;

  UPDATE battles.battles
     SET status = 'open',
         updated_at = now()
   WHERE id = p_battle_id
     AND status = 'draft';

  IF FOUND THEN
    INSERT INTO audit.events (actor_id, event_type, resource_type, resource_id, meta)
    VALUES (
      NULL,
      'battle.auto_promoted',
      'battle',
      p_battle_id,
      jsonb_build_object('readiness', v_readiness)
    );
    RETURN TRUE;
  END IF;

  RETURN FALSE;
END;
$$;


ALTER FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") IS 'CB: Promotes draft→open when readiness check passes. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_max_contenders INT;
  v_existing_count INT;
  v_to_assign      INT;
  v_assigned       INT := 0;
  rec              RECORD;
BEGIN
  -- Read max_contenders; default to 2
  SELECT COALESCE(max_contenders, 2)
    INTO v_max_contenders
    FROM battles.battles
   WHERE id = p_battle_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Battle not found' USING ERRCODE = 'P0002';
  END IF;

  SELECT COUNT(*)
    INTO v_existing_count
    FROM battles.contenders
   WHERE battle_id = p_battle_id;

  v_to_assign := v_max_contenders - v_existing_count;
  IF v_to_assign <= 0 THEN
    RETURN 0;
  END IF;

  FOR rec IN
    SELECT al.id AS lenser_id
      FROM lensers.ai_lensers al
     WHERE al.is_available = TRUE
       AND NOT EXISTS (
         SELECT 1 FROM battles.contenders c
          WHERE c.battle_id = p_battle_id
            AND c.lenser_id = al.id
       )
     ORDER BY al.last_active_at DESC NULLS LAST
     LIMIT v_to_assign
  LOOP
    INSERT INTO battles.contenders (battle_id, lenser_id, slot, status)
    VALUES (p_battle_id, rec.lenser_id,
      CASE WHEN v_assigned = 0 THEN 'A' ELSE 'B' END,
      'pending')
    ON CONFLICT DO NOTHING;
    v_assigned := v_assigned + 1;
  END LOOP;

  RETURN v_assigned;
END;
$$;


ALTER FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") IS 'CB: Assigns available AI lensers up to max_contenders for auto-assign battles.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_voter      UUID;
  v_status     TEXT;
  v_existing   battles.votes%ROWTYPE;
  v_old_cid    UUID;
BEGIN
  v_voter := lensers.get_auth_lenser_id();
  IF v_voter IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  SELECT status::text INTO v_status
    FROM battles.battles WHERE id = p_battle_id;

  IF v_status IS NULL THEN
    RAISE EXCEPTION 'battle_not_found' USING ERRCODE = '42501';
  END IF;
  IF v_status <> 'voting' THEN
    RAISE EXCEPTION 'battle_closed: status=%', v_status USING ERRCODE = 'P0001';
  END IF;

  -- contender must belong to this battle
  IF NOT EXISTS (
    SELECT 1 FROM battles.contenders
     WHERE id = p_new_contender_id AND battle_id = p_battle_id
  ) THEN
    RAISE EXCEPTION 'contender_not_in_battle' USING ERRCODE = '22023';
  END IF;

  SELECT * INTO v_existing
    FROM battles.votes
   WHERE battle_id = p_battle_id
     AND voter_lenser_id = v_voter
   FOR UPDATE;

  IF v_existing.id IS NULL THEN
    -- First vote — just delegate to fn_submit_vote semantics inline.
    INSERT INTO battles.votes (
      battle_id, voter_lenser_id, vote_value, voted_contender_id, is_draw, rationale
    )
    VALUES (
      p_battle_id, v_voter, 'contender_a'::battles.vote_value_enum,
      p_new_contender_id, false, NULL
    )
    RETURNING * INTO v_existing;

    INSERT INTO battles.vote_aggregates (
      battle_id, contender_id, raw_vote_count, weighted_vote_sum, draw_count
    ) VALUES (
      p_battle_id, p_new_contender_id, 1, 1, 0
    )
    ON CONFLICT (battle_id, contender_id) DO UPDATE SET
      raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
      weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
      updated_at        = now();

    UPDATE battles.battles
       SET total_vote_count = total_vote_count + 1
     WHERE id = p_battle_id;

    RETURN jsonb_build_object(
      'vote_id',    v_existing.id,
      'updated_at', v_existing.updated_at,
      'status',     'created'
    );
  END IF;

  v_old_cid := v_existing.voted_contender_id;
  IF v_old_cid IS NOT DISTINCT FROM p_new_contender_id THEN
    -- No-op; touch updated_at so callers can see the action.
    UPDATE battles.votes
       SET updated_at = now()
     WHERE id = v_existing.id
     RETURNING updated_at INTO v_existing.updated_at;

    RETURN jsonb_build_object(
      'vote_id',    v_existing.id,
      'updated_at', v_existing.updated_at,
      'status',     'unchanged'
    );
  END IF;

  -- Swap: decrement the old aggregate, increment the new one, update the row.
  UPDATE battles.vote_aggregates
     SET raw_vote_count    = GREATEST(raw_vote_count    - 1, 0),
         weighted_vote_sum = GREATEST(weighted_vote_sum - 1, 0),
         updated_at        = now()
   WHERE battle_id = p_battle_id
     AND contender_id = v_old_cid;

  INSERT INTO battles.vote_aggregates (
    battle_id, contender_id, raw_vote_count, weighted_vote_sum, draw_count
  ) VALUES (
    p_battle_id, p_new_contender_id, 1, 1, 0
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE SET
    raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
    weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
    updated_at        = now();

  UPDATE battles.votes
     SET voted_contender_id = p_new_contender_id
   WHERE id = v_existing.id
   RETURNING * INTO v_existing;

  RETURN jsonb_build_object(
    'vote_id',    v_existing.id,
    'updated_at', v_existing.updated_at,
    'status',     'changed'
  );
END $$;


ALTER FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_battle         RECORD;
  v_contender_cnt  INT;
  v_blockers       TEXT[] := '{}';
  v_render_ok      BOOLEAN := TRUE;
  v_byok_ok        BOOLEAN := TRUE;
  v_byok_result    JSONB;
  rec              RECORD;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    RETURN jsonb_build_object('ready', false, 'blockers', ARRAY['battle_not_found']);
  END IF;

  -- Check template valid (template_id set)
  IF v_battle.template_id IS NULL AND (v_battle.task_prompt IS NULL OR v_battle.task_prompt = '') THEN
    v_blockers := v_blockers || ARRAY['no_template_or_task_prompt'];
  END IF;

  -- Check render prompt succeeds (only if template_id set)
  IF v_battle.template_id IS NOT NULL THEN
    BEGIN
      PERFORM public.fn_battles_render_prompt(p_battle_id, '{}'::JSONB);
    EXCEPTION WHEN OTHERS THEN
      v_render_ok := FALSE;
      v_blockers  := v_blockers || ARRAY[CONCAT('prompt_render_failed:', SQLERRM)];
    END;
  END IF;

  -- Check ≥2 contenders assigned
  SELECT COUNT(*) INTO v_contender_cnt
    FROM battles.contenders
   WHERE battle_id = p_battle_id;

  IF v_contender_cnt < 2 THEN
    v_blockers := v_blockers || ARRAY[CONCAT('insufficient_contenders:', v_contender_cnt::TEXT)];
  END IF;

  -- Check BYOK valid for AI contenders
  FOR rec IN
    SELECT c.id AS contender_id
      FROM battles.contenders c
     WHERE c.battle_id = p_battle_id
       AND c.contender_type IN ('ai_model', 'ai_agent')
  LOOP
    v_byok_result := public.fn_byok_validate_for_battle(p_battle_id, rec.contender_id);
    IF NOT (v_byok_result ->> 'valid')::BOOLEAN THEN
      v_blockers := v_blockers || ARRAY[
        CONCAT('byok_invalid:contender=', rec.contender_id,
               ':reason=', v_byok_result ->> 'reason')
      ];
      v_byok_ok := FALSE;
    END IF;
  END LOOP;

  RETURN jsonb_build_object(
    'ready',    array_length(v_blockers, 1) IS NULL,
    'blockers', v_blockers
  );
END;
$$;


ALTER FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") IS 'CB: Returns {ready, blockers[]} for a battle. Used before auto-promote.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_source RECORD;
    v_new_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_source
    FROM battles.battles
    WHERE id = p_battle_id
      AND deleted_at IS NULL;

    IF v_source IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    INSERT INTO battles.battles (
        creator_lenser_id, title, slug, task_prompt,
        rubric_id, status, max_contenders
    )
    VALUES (
        v_lenser_id, p_title, p_slug, v_source.task_prompt,
        v_source.rubric_id, 'draft', v_source.max_contenders
    )
    RETURNING id INTO v_new_id;

    -- Log the event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        v_new_id,
        'status_change',
        v_lenser_id,
        jsonb_build_object('action', 'cloned', 'source_battle_id', p_battle_id)
    );

    RETURN v_new_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_lenser_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id != v_lenser_id THEN
        RAISE EXCEPTION 'Only the battle creator can close';
    END IF;

    IF v_battle.status NOT IN ('open', 'scoring') THEN
        RAISE EXCEPTION 'Battle must be in open or scoring status to close (current: %)', v_battle.status;
    END IF;

    UPDATE battles.battles
    SET status = 'closed',
        finalized_at = now(),
        updated_at = now()
    WHERE id = p_battle_id;

    -- Log the event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        p_battle_id,
        'status_change',
        v_lenser_id,
        jsonb_build_object('from', v_battle.status::text, 'to', 'closed')
    );
END;
$$;


ALTER FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id   uuid;
  v_battle_id   uuid;
  v_count       integer;
  v_oldest_at   timestamptz;
  v_retry_after integer;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT count(*), min(created_at)
    INTO v_count, v_oldest_at
    FROM battles.battles
   WHERE creator_lenser_id = v_lenser_id
     AND created_at > now() - interval '1 hour';

  IF v_count >= 20 THEN
    v_retry_after := GREATEST(0, EXTRACT(epoch FROM (v_oldest_at + interval '1 hour' - now()))::integer);
    RAISE EXCEPTION 'battle_rate_limit_exceeded'
      USING HINT   = 'p0429',
            DETAIL = '{"retry_after":' || v_retry_after || '}';
  END IF;

  INSERT INTO battles.battles (creator_lenser_id, title, slug, task_prompt, rubric_id, status)
  VALUES (v_lenser_id, p_title, p_slug, p_task_prompt, p_rubric_id, 'draft')
  RETURNING id INTO v_battle_id;

  RETURN v_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_template RECORD;
  v_lenser_id uuid;
  v_battle_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();

  SELECT * INTO v_template
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL
     AND (is_public = true OR creator_lenser_id = v_lenser_id);

  IF v_template IS NULL THEN
    RAISE EXCEPTION 'Template not found or not accessible';
  END IF;

  INSERT INTO battles.battles (
    creator_lenser_id, title, slug, task_prompt, rubric_id,
    status, max_contenders, template_id
  )
  VALUES (
    v_lenser_id, p_title, p_slug, v_template.task_prompt,
    v_template.rubric_id, 'draft', v_template.max_contenders, p_template_id
  )
  RETURNING id INTO v_battle_id;

  RETURN v_battle_id;
END $$;


ALTER FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller   uuid;
  v_owner    uuid;
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
  END IF;

  SELECT creator_lenser_id INTO v_owner
    FROM battles.battles
   WHERE id = p_parent_id
     AND deleted_at IS NULL;

  IF v_owner IS NULL THEN
    RAISE EXCEPTION 'parent_battle_not_found: %', p_parent_id
      USING ERRCODE = 'P0002';
  END IF;

  IF v_owner IS DISTINCT FROM v_caller THEN
    RAISE EXCEPTION 'not_battle_owner' USING ERRCODE = '42501';
  END IF;

  RETURN battles.fn_clone_battle_for_rematch(p_parent_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") IS 'Phase V1: authenticated wrapper. Creates a draft rematch of a terminal parent battle the caller owns. Clones structural fields and rotates contender slots. Returns the new battle id.';



CREATE TABLE IF NOT EXISTS "battles"."templates" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "creator_lenser_id" "uuid" NOT NULL,
    "title" "text" NOT NULL,
    "description" "text",
    "task_prompt" "text" NOT NULL,
    "rubric_id" "uuid",
    "max_contenders" integer DEFAULT 2 NOT NULL,
    "is_public" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "deleted_at" timestamp with time zone,
    "recurrence_rule" "text",
    "next_run_at" timestamp with time zone,
    "auto_start_delay_hours" integer DEFAULT 1 NOT NULL,
    "category" "text",
    CONSTRAINT "ck_battles_templates_description_len" CHECK ((("description" IS NULL) OR ("length"("description") <= 2000))),
    CONSTRAINT "ck_battles_templates_task_prompt_len" CHECK ((("task_prompt" IS NULL) OR ("length"("task_prompt") <= 32000))),
    CONSTRAINT "ck_battles_templates_title_len" CHECK ((("title" IS NULL) OR ("length"("title") <= 200))),
    CONSTRAINT "templates_category_check" CHECK ((("category" IS NULL) OR (("char_length"("category") >= 1) AND ("char_length"("category") <= 32)))),
    CONSTRAINT "templates_max_contenders_check" CHECK (("max_contenders" >= 2))
);


ALTER TABLE "battles"."templates" OWNER TO "postgres";


COMMENT ON COLUMN "battles"."templates"."category" IS 'Optional taxonomy bucket for the public template gallery (Phase AW). Examples: ''creative'', ''technical'', ''business'', ''gaming''. NULL = uncategorized.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_task_prompt" "text" DEFAULT ''::"text", "p_category" "text" DEFAULT NULL::"text", "p_max_contenders" integer DEFAULT 2, "p_is_public" boolean DEFAULT false) RETURNS "battles"."templates"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_row       battles.templates%ROWTYPE;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_title IS NULL OR char_length(trim(p_title)) = 0 THEN
    RAISE EXCEPTION 'title_required' USING ERRCODE = '22023';
  END IF;
  IF p_task_prompt IS NULL OR char_length(trim(p_task_prompt)) = 0 THEN
    RAISE EXCEPTION 'task_prompt_required' USING ERRCODE = '22023';
  END IF;
  IF p_category IS NOT NULL
     AND p_category NOT IN ('creative','technical','business','gaming') THEN
    RAISE EXCEPTION 'invalid_category: %', p_category USING ERRCODE = '22023';
  END IF;
  IF p_max_contenders < 2 THEN
    RAISE EXCEPTION 'max_contenders_too_small' USING ERRCODE = '22023';
  END IF;

  INSERT INTO battles.templates (
    creator_lenser_id, title, description, task_prompt,
    category, max_contenders, is_public
  ) VALUES (
    v_lenser_id, p_title, p_description, p_task_prompt,
    p_category, p_max_contenders, COALESCE(p_is_public, false)
  )
  RETURNING * INTO v_row;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_delete('battle', p_battle_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  UPDATE battles.templates
     SET deleted_at = now(),
         updated_at = now(),
         is_public  = false
   WHERE id                = p_template_id
     AND creator_lenser_id = v_lenser_id
     AND deleted_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_owned' USING ERRCODE = '42501';
  END IF;
END $$;


ALTER FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'xp', 'reputation'
    AS $$
DECLARE
    v_battle RECORD;
    v_winner_id uuid;
    v_top RECORD;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status NOT IN ('voting', 'scoring') THEN
        RAISE EXCEPTION 'Battle must be in voting or scoring status to finalize (current: %)', v_battle.status;
    END IF;

    SELECT contender_id, raw_vote_count
    INTO v_top
    FROM battles.vote_aggregates
    WHERE battle_id = p_battle_id
    ORDER BY raw_vote_count DESC
    LIMIT 1;

    IF v_top IS NOT NULL THEN
        DECLARE
            v_tie_count int;
        BEGIN
            SELECT COUNT(*) INTO v_tie_count
            FROM battles.vote_aggregates
            WHERE battle_id = p_battle_id
              AND raw_vote_count = v_top.raw_vote_count;

            IF v_tie_count > 1 THEN
                v_winner_id := NULL;
            ELSE
                v_winner_id := v_top.contender_id;
            END IF;
        END;
    END IF;

    UPDATE battles.vote_aggregates va
    SET rank_position = sub.rk
    FROM (
        SELECT contender_id,
               ROW_NUMBER() OVER (ORDER BY raw_vote_count DESC, weighted_vote_sum DESC) AS rk
        FROM battles.vote_aggregates
        WHERE battle_id = p_battle_id
    ) sub
    WHERE va.battle_id = p_battle_id
      AND va.contender_id = sub.contender_id;

    UPDATE battles.battles
    SET status = 'closed',
        winner_contender_id = v_winner_id,
        finalized_at = now(),
        updated_at = now()
    WHERE id = p_battle_id;

    -- Phase O3: per-battle ELO update. Best-effort — never roll back finalize.
    BEGIN
      PERFORM public.fn_compute_elo_after_battle(p_battle_id);
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_compute_elo_after_battle failed for battle %: %', p_battle_id, SQLERRM;
    END;
END;
$$;


ALTER FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") RETURNS TABLE("contender_id" "uuid", "vote_value" "text", "is_draw" boolean, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT
    v.voted_contender_id AS contender_id,
    v.vote_value::text   AS vote_value,
    v.is_draw,
    v.updated_at
  FROM battles.votes v
  WHERE v.battle_id       = p_battle_id
    AND v.voter_lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_contenders jsonb;
    v_aggregates jsonb;
    v_result jsonb;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles
    WHERE id = p_battle_id
      AND status IN ('open', 'voting', 'scoring', 'closed', 'published')
      AND deleted_at IS NULL;

    IF v_battle IS NULL THEN
        RETURN NULL;
    END IF;

    -- Build contenders array (no actor_id)
    SELECT COALESCE(jsonb_agg(jsonb_build_object(
        'id', c.id,
        'slot', c.slot,
        'contender_type', c.contender_type::text,
        'display_name', c.display_name,
        'contender_status', c.contender_status
    ) ORDER BY c.slot), '[]'::jsonb)
    INTO v_contenders
    FROM battles.contenders c
    WHERE c.battle_id = p_battle_id;

    -- Build vote aggregates array
    SELECT COALESCE(jsonb_agg(jsonb_build_object(
        'contender_id', va.contender_id,
        'raw_vote_count', va.raw_vote_count,
        'weighted_vote_sum', va.weighted_vote_sum,
        'draw_count', va.draw_count,
        'rank_position', va.rank_position
    ) ORDER BY va.rank_position NULLS LAST), '[]'::jsonb)
    INTO v_aggregates
    FROM battles.vote_aggregates va
    WHERE va.battle_id = p_battle_id;

    -- Compose result
    v_result := jsonb_build_object(
        'id', v_battle.id,
        'title', v_battle.title,
        'slug', v_battle.slug,
        'status', v_battle.status,
        'task_prompt', v_battle.task_prompt,
        'max_contenders', v_battle.max_contenders,
        'invite_code', v_battle.invite_code,
        'total_vote_count', v_battle.total_vote_count,
        'winner_contender_id', v_battle.winner_contender_id,
        'voting_opens_at', v_battle.voting_opens_at,
        'voting_closes_at', v_battle.voting_closes_at,
        'published_at', v_battle.published_at,
        'finalized_at', v_battle.finalized_at,
        'created_at', v_battle.created_at,
        'contenders', v_contenders,
        'vote_aggregates', v_aggregates
    );

    RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "owner_id" "uuid", "webhook_url" "text", "event_types" "text"[], "created_at" timestamp with time zone, "revoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_actor UUID;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  RETURN QUERY
    SELECT
      s.id,
      s.battle_id,
      s.owner_id,
      s.webhook_url,
      s.event_types,
      s.created_at,
      s.revoked_at
    FROM battles.battle_event_subscriptions s
   WHERE s.battle_id = p_battle_id
     AND s.owner_id  = v_actor
   ORDER BY s.created_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") IS 'Returns caller-owned webhook subscriptions for a battle. secret_hmac excluded.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") RETURNS "battles"."templates"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_row       battles.templates%ROWTYPE;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  SELECT * INTO v_row
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL
     AND (creator_lenser_id = v_lenser_id OR is_public = true);

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_found' USING ERRCODE = '42501';
  END IF;
  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_battle RECORD;
    v_invited_lenser_id uuid;
    v_invitation_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id != v_lenser_id THEN
        RAISE EXCEPTION 'Only the battle creator can invite';
    END IF;

    IF v_battle.status NOT IN ('draft', 'open') THEN
        RAISE EXCEPTION 'Battle must be in draft or open status to invite (current: %)', v_battle.status;
    END IF;

    -- Try to resolve email to a lenser profile
    SELECT p.id INTO v_invited_lenser_id
    FROM lensers.profiles p
    JOIN auth.users u ON u.id = p.user_id
    WHERE u.email = p_email
    LIMIT 1;

    INSERT INTO battles.invitations (battle_id, invited_by, invited_email, invited_lenser_id)
    VALUES (p_battle_id, v_lenser_id, p_email, v_invited_lenser_id)
    RETURNING id INTO v_invitation_id;

    -- Log the event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        p_battle_id,
        'invitation_sent',
        v_lenser_id,
        jsonb_build_object('invitation_id', v_invitation_id, 'email', p_email)
    );

    RETURN v_invitation_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_lenser RECORD;
    v_battle RECORD;
    v_count int;
    v_next_slot char(1);
    v_contender_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT display_name INTO v_lenser
    FROM lensers.profiles WHERE id = v_lenser_id;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle is not open for joining';
    END IF;

    -- Check if already a contender
    IF EXISTS (
        SELECT 1 FROM battles.contenders
        WHERE battle_id = p_battle_id
          AND contender_ref_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'Already a contender in this battle';
    END IF;

    -- Check contender count
    SELECT COUNT(*) INTO v_count
    FROM battles.contenders WHERE battle_id = p_battle_id;

    IF v_count >= v_battle.max_contenders THEN
        RAISE EXCEPTION 'Battle is full (% / %)', v_count, v_battle.max_contenders;
    END IF;

    -- Assign next slot (A, B, C, ...)
    v_next_slot := chr(65 + v_count);

    INSERT INTO battles.contenders (
        battle_id, slot, contender_type, contender_ref_id,
        display_name, entry_mode, contender_status, joined_at
    )
    VALUES (
        p_battle_id, v_next_slot, 'human', v_lenser_id,
        v_lenser.display_name, 'direct', 'active', now()
    )
    RETURNING id INTO v_contender_id;

    -- Auto-create pending submission
    INSERT INTO battles.submissions (battle_id, contender_id, status)
    VALUES (p_battle_id, v_contender_id, 'pending');

    RETURN v_contender_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid" DEFAULT NULL::"uuid", "p_runner_mode" "text" DEFAULT 'cloud'::"text", "p_device_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id    UUID;
  v_contender_id UUID;
  v_battle       battles.battles%ROWTYPE;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'battles_join_unauthenticated';
  END IF;

  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'battles_join_not_found: %', p_battle_id;
  END IF;

  IF v_battle.status NOT IN ('open', 'executing') THEN
    RAISE EXCEPTION 'battles_join_not_open: status=%', v_battle.status;
  END IF;

  -- Idempotent: return existing contender if already joined
  SELECT id INTO v_contender_id
  FROM   battles.contenders
  WHERE  battle_id = p_battle_id
  AND    lenser_id = v_lenser_id
  LIMIT  1;

  IF v_contender_id IS NOT NULL THEN
    RETURN v_contender_id;
  END IF;

  -- Check max contenders
  IF (SELECT count(*) FROM battles.contenders WHERE battle_id = p_battle_id) >= v_battle.max_contenders THEN
    RAISE EXCEPTION 'battles_join_full: max_contenders=%', v_battle.max_contenders;
  END IF;

  INSERT INTO battles.contenders (
    battle_id,
    lenser_id,
    contender_status
  )
  VALUES (
    p_battle_id,
    v_lenser_id,
    'active'
  )
  RETURNING id INTO v_contender_id;

  INSERT INTO audit.events (event_type, actor_type, actor_id, payload)
  VALUES (
    'battle.joined',
    'lenser',
    v_lenser_id,
    jsonb_build_object(
      'battle_id',    p_battle_id,
      'agent_id',     p_agent_id,
      'runner_mode',  p_runner_mode,
      'device_id',    p_device_id,
      'workflow_id',  p_workflow_id
    )
  );

  RETURN v_contender_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") RETURNS TABLE("rank" integer, "contender_id" "uuid", "display_name" "text", "score" numeric, "vote_count" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
    RETURN QUERY
    SELECT
        COALESCE(va.rank_position, 0)::integer AS rank,
        c.id AS contender_id,
        c.display_name,
        va.weighted_vote_sum AS score,
        va.raw_vote_count AS vote_count
    FROM battles.contenders c
    LEFT JOIN battles.vote_aggregates va
        ON va.battle_id = c.battle_id AND va.contender_id = c.id
    WHERE c.battle_id = p_battle_id
    ORDER BY va.rank_position NULLS LAST, va.raw_vote_count DESC NULLS LAST;
END;
$$;


ALTER FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  UPDATE battles.battles
  SET forum_thread_id = p_forum_thread_id
  WHERE id = p_battle_id
    AND creator_lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_list_public"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "title" "text", "slug" "text", "status" "text", "contender_count" bigint, "total_vote_count" integer, "creator_display_name" "text", "created_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
BEGIN
    RETURN QUERY
    SELECT
        b.id,
        b.title,
        b.slug,
        b.status::text,
        (SELECT COUNT(*) FROM battles.contenders c WHERE c.battle_id = b.id) AS contender_count,
        b.total_vote_count,
        p.display_name AS creator_display_name,
        b.created_at,
        b.voting_opens_at,
        b.voting_closes_at
    FROM battles.battles b
    JOIN lensers.profiles p ON p.id = b.creator_lenser_id
    WHERE b.status IN ('open', 'voting', 'scoring', 'closed', 'published')
      AND b.deleted_at IS NULL
    ORDER BY b.created_at DESC
    LIMIT p_limit
    OFFSET p_offset;
END;
$$;


ALTER FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_battle           RECORD;
  v_lenser_id        UUID;
  v_was_contender    BOOLEAN := false;
  v_was_voter        BOOLEAN := false;
  v_category         TEXT;
BEGIN
  SELECT b.id, b.status::text AS status, b.template_id, b.slug, t.category
    INTO v_battle
    FROM battles.battles b
    LEFT JOIN battles.templates t ON t.id = b.template_id
   WHERE b.id = p_battle_id
     AND b.deleted_at IS NULL;

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  IF v_battle.status <> 'closed' THEN
    RETURN NULL;
  END IF;

  -- Best-effort resolve caller identity. Anonymous callers leave v_lenser_id
  -- as NULL and fall through to the "create" branch.
  BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
  EXCEPTION WHEN OTHERS THEN
    v_lenser_id := NULL;
  END;

  IF v_lenser_id IS NOT NULL THEN
    SELECT EXISTS (
      SELECT 1 FROM battles.contenders c
       WHERE c.battle_id = p_battle_id
         AND c.contender_ref_id = v_lenser_id
    ) INTO v_was_contender;

    IF NOT v_was_contender THEN
      SELECT EXISTS (
        SELECT 1 FROM battles.votes v
         WHERE v.battle_id = p_battle_id
           AND v.voter_lenser_id = v_lenser_id
      ) INTO v_was_voter;
    END IF;
  END IF;

  v_category := v_battle.category;

  IF v_was_contender THEN
    RETURN jsonb_build_object(
      'action',     'rematch',
      'battle_id',  p_battle_id,
      'slug',       v_battle.slug
    );
  ELSIF v_was_voter THEN
    RETURN jsonb_build_object(
      'action',     'browse',
      'category',   v_category
    );
  ELSE
    RETURN jsonb_build_object(
      'action',      'create',
      'template_id', v_battle.template_id
    );
  END IF;
END $$;


ALTER FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") IS 'Phase BX: contextual next-action recommendation for a closed battle. Returns NULL when battle is not closed or not found. Resolves the caller as contender / voter / other and emits a single CTA shape.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  rec RECORD;
BEGIN
  FOR rec IN
    SELECT id
      FROM battles.battle_event_subscriptions
     WHERE battle_id  = p_battle_id
       AND revoked_at IS NULL
       AND p_event_type = ANY(event_types)
  LOOP
    INSERT INTO audit.webhook_outbox (subscription_id, battle_id, event_type, payload)
    VALUES (rec.id, p_battle_id, p_event_type, p_payload);
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") IS 'CB: Inserts webhook_outbox entries for matching active subscriptions.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_code text;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles
    WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Only the battle creator can open a battle';
    END IF;

    IF v_battle.status <> 'draft' THEN
        RAISE EXCEPTION 'Battle must be in draft status to open (current: %)', v_battle.status;
    END IF;

    -- Generate invite code if not set
    v_code := v_battle.invite_code;
    IF v_code IS NULL THEN
        v_code := upper(substr(md5(gen_random_uuid()::text), 1, 8));
    END IF;

    UPDATE battles.battles
    SET status = 'open', invite_code = v_code, updated_at = now()
    WHERE id = p_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_lenser_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id != v_lenser_id THEN
        RAISE EXCEPTION 'Only the battle creator can publish';
    END IF;

    IF v_battle.status != 'closed' THEN
        RAISE EXCEPTION 'Battle must be in closed status to publish (current: %)', v_battle.status;
    END IF;

    UPDATE battles.battles
    SET status = 'published',
        published_at = now(),
        updated_at = now()
    WHERE id = p_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_template     RECORD;
  v_rendered     TEXT;
  v_var          RECORD;
  v_value        TEXT;
BEGIN
  SELECT id, task_prompt, creator_lenser_id, is_public
    INTO v_template
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_found' USING ERRCODE = '42501';
  END IF;

  -- Accessibility: owner OR public template OR anonymous read of public.
  IF NOT v_template.is_public
     AND v_template.creator_lenser_id IS DISTINCT FROM auth.uid() THEN
    RAISE EXCEPTION 'template_not_accessible' USING ERRCODE = '42501';
  END IF;

  v_rendered := v_template.task_prompt;

  FOR v_var IN
    SELECT variable_key, default_value, required
      FROM battles.template_prompt_variables
     WHERE template_id = p_template_id
     ORDER BY ordinal, variable_key
  LOOP
    v_value := COALESCE(
      NULLIF(p_variables->>v_var.variable_key, ''),
      v_var.default_value
    );

    IF v_value IS NULL THEN
      IF v_var.required THEN
        RAISE EXCEPTION 'missing_required_variable: %', v_var.variable_key
          USING ERRCODE = '22023';
      END IF;
      v_value := '';
    END IF;

    -- Plain text substitution, including the braces.
    v_rendered := REPLACE(
      v_rendered,
      '{{' || v_var.variable_key || '}}',
      v_value
    );
  END LOOP;

  RETURN v_rendered;
END $$;


ALTER FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('battle', p_battle_id); $$;


ALTER FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_archive('battle', p_battle_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_actor UUID;
  v_rows  INT;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  UPDATE battles.battle_event_subscriptions
     SET revoked_at = now()
   WHERE id       = p_subscription_id
     AND owner_id = v_actor
     AND revoked_at IS NULL;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  IF v_rows = 0 THEN
    RAISE EXCEPTION 'Subscription not found or already revoked'
      USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") IS 'Soft-revokes a caller-owned webhook subscription. Idempotent-safe (raises P0002 if already revoked).';



CREATE OR REPLACE FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers'
    AS $_$
DECLARE
  v_caller    uuid;
  v_owner     uuid;
  v_series_id uuid;
  v_slug      text;
  v_parts     text[];
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
  END IF;

  IF p_name IS NULL OR length(btrim(p_name)) = 0 THEN
    RAISE EXCEPTION 'series_name_required' USING ERRCODE = '22023';
  END IF;

  v_parts := regexp_split_to_array(btrim(COALESCE(p_cron_expr, '')), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RAISE EXCEPTION 'invalid_cron_expression: expected 5 fields'
      USING ERRCODE = '22023';
  END IF;

  SELECT creator_lenser_id INTO v_owner
    FROM battles.battles
   WHERE id = p_seed_battle_id
     AND deleted_at IS NULL;

  IF v_owner IS NULL THEN
    RAISE EXCEPTION 'seed_battle_not_found: %', p_seed_battle_id
      USING ERRCODE = 'P0002';
  END IF;

  IF v_owner IS DISTINCT FROM v_caller THEN
    RAISE EXCEPTION 'not_battle_owner' USING ERRCODE = '42501';
  END IF;

  -- Slug: kebab-case from name + 6 random hex; bounded to <= 128 chars.
  v_slug := substr(
    regexp_replace(
      regexp_replace(lower(p_name), '[^a-z0-9]+', '-', 'g'),
      '(^-+|-+$)', '', 'g'
    ),
    1, 110
  );
  IF length(v_slug) = 0 THEN
    v_slug := 'series';
  END IF;
  v_slug := v_slug || '-' || substr(replace(gen_random_uuid()::text, '-', ''), 1, 6);

  INSERT INTO battles.series (
    creator_lenser_id, name, slug, seed_battle_id, cron_expr,
    next_dispatch_at, is_active
  )
  VALUES (
    v_caller, btrim(p_name), v_slug, p_seed_battle_id, btrim(p_cron_expr),
    -- Earliest the dispatcher can pick it up: the next top-of-hour tick.
    date_trunc('hour', now()) + interval '1 hour',
    true
  )
  RETURNING id INTO v_series_id;

  -- Seed at position=1.
  INSERT INTO battles.series_battles (series_id, battle_id, position)
  VALUES (v_series_id, p_seed_battle_id, 1);

  RETURN v_series_id;
END;
$_$;


ALTER FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") IS 'Phase V4: create a battle series anchored to a seed battle the caller owns. Inserts the seed at position=1 and primes next_dispatch_at to the next top-of-hour tick. Returns the new series id.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_pending int;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Only the battle creator can start voting';
    END IF;

    IF v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle must be open to start voting (current: %)', v_battle.status;
    END IF;

    -- Check all contenders have submitted
    SELECT COUNT(*) INTO v_pending
    FROM battles.submissions s
    WHERE s.battle_id = p_battle_id AND s.status = 'pending';

    IF v_pending > 0 THEN
        RAISE EXCEPTION '% contender(s) have not submitted yet', v_pending;
    END IF;

    UPDATE battles.battles
    SET status = 'voting',
        voting_opens_at = now(),
        voting_closes_at = p_voting_closes_at,
        updated_at = now()
    WHERE id = p_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text" DEFAULT NULL::"text", "p_content_url" "text" DEFAULT NULL::"text", "p_content_media" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_contender RECORD;
    v_battle RECORD;
    v_submission_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL OR v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle is not open for submissions';
    END IF;

    SELECT * INTO v_contender
    FROM battles.contenders
    WHERE battle_id = p_battle_id
      AND contender_type = 'human'
      AND contender_ref_id = v_lenser_id;

    IF v_contender IS NULL THEN
        RAISE EXCEPTION 'You are not a contender in this battle';
    END IF;

    IF p_content_text IS NULL AND p_content_url IS NULL THEN
        RAISE EXCEPTION 'At least content_text or content_url is required';
    END IF;

    UPDATE battles.submissions
    SET status = 'submitted',
        content_text = p_content_text,
        content_url = p_content_url,
        content_media = COALESCE(p_content_media, '[]'::jsonb),
        submitted_at = now(),
        updated_at = now()
    WHERE battle_id = p_battle_id AND contender_id = v_contender.id
    RETURNING id INTO v_submission_id;

    RETURN v_submission_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text" DEFAULT NULL::"text", "p_content_url" "text" DEFAULT NULL::"text", "p_content_media" "jsonb" DEFAULT NULL::"jsonb", "p_execution_run_id" "uuid" DEFAULT NULL::"uuid", "p_artifact_id" "uuid" DEFAULT NULL::"uuid", "p_source_type" "text" DEFAULT 'manual'::"text", "p_adapter_id" "uuid" DEFAULT NULL::"uuid", "p_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'execution'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_contender RECORD;
    v_battle RECORD;
    v_submission_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL OR v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle is not open for submissions';
    END IF;

    -- Look up contender by contender_ref_id only
    SELECT * INTO v_contender
    FROM battles.contenders
    WHERE battle_id = p_battle_id
      AND contender_ref_id = v_lenser_id;

    IF v_contender IS NULL THEN
        RAISE EXCEPTION 'You are not a contender in this battle';
    END IF;

    IF p_content_text IS NULL AND p_content_url IS NULL AND p_execution_run_id IS NULL THEN
        RAISE EXCEPTION 'At least content_text, content_url, or execution_run_id is required';
    END IF;

    UPDATE battles.submissions
    SET status = 'submitted',
        content_text = p_content_text,
        content_url = p_content_url,
        content_media = COALESCE(p_content_media, '[]'::jsonb),
        submitted_at = now(),
        updated_at = now(),
        execution_run_id = p_execution_run_id,
        artifact_id = p_artifact_id,
        source_type = p_source_type,
        adapter_id = p_adapter_id,
        model_id = p_model_id
    WHERE battle_id = p_battle_id AND contender_id = v_contender.id
    RETURNING id INTO v_submission_id;

    -- Link execution to submission if provided
    IF p_execution_run_id IS NOT NULL THEN
        INSERT INTO execution.links (run_id, entity_type, entity_id)
        VALUES (p_execution_run_id, 'submission', v_submission_id)
        ON CONFLICT DO NOTHING;
    END IF;

    RETURN v_submission_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."submissions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "status" "battles"."submission_status_enum" DEFAULT 'pending'::"battles"."submission_status_enum" NOT NULL,
    "content_text" "text",
    "content_url" "text",
    "content_media" "jsonb" DEFAULT '[]'::"jsonb",
    "submitted_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "execution_run_id" "uuid",
    "artifact_id" "uuid",
    "source_type" "text" DEFAULT 'manual'::"text" NOT NULL,
    "model_id" "uuid",
    "integrity_hash" "text",
    "is_final" boolean DEFAULT true NOT NULL,
    "revision_of_id" "uuid",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "workflow_id" "uuid",
    "submission_type" "text" DEFAULT 'text'::"text" NOT NULL,
    "media_url" "text",
    "mime_type" "text",
    "output_modality" "text",
    "adapter_id" "uuid",
    CONSTRAINT "ck_battles_submissions_content_text_len" CHECK ((("content_text" IS NULL) OR ("length"("content_text") <= 20000))),
    CONSTRAINT "submissions_content_required" CHECK ((("status" = 'pending'::"battles"."submission_status_enum") OR ("content_text" IS NOT NULL) OR ("content_url" IS NOT NULL))),
    CONSTRAINT "submissions_mime_type_check" CHECK ((("mime_type" IS NULL) OR ("char_length"("mime_type") <= 128))),
    CONSTRAINT "submissions_output_modality_check" CHECK ((("output_modality" IS NULL) OR ("output_modality" = ANY (ARRAY['text'::"text", 'image'::"text", 'video'::"text", 'audio'::"text"])))),
    CONSTRAINT "submissions_source_type_check" CHECK (("source_type" = ANY (ARRAY['manual'::"text", 'execution_output'::"text", 'hybrid'::"text", 'imported'::"text"]))),
    CONSTRAINT "submissions_submission_type_check" CHECK (("submission_type" = ANY (ARRAY['text'::"text", 'workflow'::"text", 'lens'::"text"])))
);


ALTER TABLE "battles"."submissions" OWNER TO "postgres";


COMMENT ON COLUMN "battles"."submissions"."workflow_id" IS 'Workflow that produced or defines this submission. Non-null when submission_type=workflow.';



COMMENT ON COLUMN "battles"."submissions"."submission_type" IS 'Content type of the submission: text (default), workflow (DAG output), lens (lens output).';



COMMENT ON COLUMN "battles"."submissions"."media_url" IS 'Phase AY: signed URL (or proxy URL) pointing at the media artifact produced by the contender. NULL for plain-text submissions.';



COMMENT ON COLUMN "battles"."submissions"."output_modality" IS 'Phase AY: discriminator the UI uses to pick the right renderer.';



COMMENT ON COLUMN "battles"."submissions"."adapter_id" IS 'Execution runner that produced this submission (execution.runners.id). NULL for manual human entries. Set by fn_battles_submit when p_adapter_id is provided.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") RETURNS "battles"."submissions"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid       UUID := auth.uid();
  v_row       battles.submissions%ROWTYPE;
  v_is_owner  BOOLEAN;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_output_modality IS NULL
     OR p_output_modality NOT IN ('image', 'video', 'audio') THEN
    RAISE EXCEPTION 'invalid_output_modality: %', p_output_modality
      USING ERRCODE = '22023';
  END IF;
  IF p_media_url IS NULL OR char_length(p_media_url) = 0 THEN
    RAISE EXCEPTION 'media_url_required' USING ERRCODE = '22023';
  END IF;

  SELECT EXISTS (
    SELECT 1
      FROM battles.contenders c
      JOIN battles.contender_entity_map cem ON cem.contender_id = c.id
     WHERE c.id           = p_contender_id
       AND c.battle_id    = p_battle_id
       AND cem.profile_id = v_uid
  )
  INTO v_is_owner;

  IF NOT v_is_owner THEN
    RAISE EXCEPTION 'contender_not_owned' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.submissions (
    battle_id,
    contender_id,
    content_url,
    media_url,
    mime_type,
    output_modality,
    status
  ) VALUES (
    p_battle_id,
    p_contender_id,
    p_media_url,
    p_media_url,
    p_mime_type,
    p_output_modality,
    'submitted'
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE
    SET content_url     = EXCLUDED.content_url,
        media_url       = EXCLUDED.media_url,
        mime_type       = EXCLUDED.mime_type,
        output_modality = EXCLUDED.output_modality,
        status          = 'submitted',
        submitted_at    = COALESCE(battles.submissions.submitted_at, now())
  RETURNING * INTO v_row;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_actor UUID;
  v_sub_id UUID;
  v_secret TEXT;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  -- Generate HMAC-style secret (random 32 bytes hex-encoded)
  v_secret := encode(gen_random_bytes(32), 'hex');

  INSERT INTO battles.battle_event_subscriptions
    (battle_id, owner_id, webhook_url, event_types, secret_hmac)
  VALUES
    (p_battle_id, v_actor, p_webhook_url, p_event_types, v_secret)
  RETURNING id INTO v_sub_id;

  RETURN v_sub_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) IS 'CB: Creates a webhook subscription with generated HMAC secret. Owner-only.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text" DEFAULT NULL::"text", "p_description" "text" DEFAULT NULL::"text", "p_task_prompt" "text" DEFAULT NULL::"text", "p_category" "text" DEFAULT NULL::"text", "p_max_contenders" integer DEFAULT NULL::integer, "p_is_public" boolean DEFAULT NULL::boolean) RETURNS "battles"."templates"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_row       battles.templates%ROWTYPE;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_category IS NOT NULL
     AND p_category NOT IN ('creative','technical','business','gaming') THEN
    RAISE EXCEPTION 'invalid_category: %', p_category USING ERRCODE = '22023';
  END IF;
  IF p_max_contenders IS NOT NULL AND p_max_contenders < 2 THEN
    RAISE EXCEPTION 'max_contenders_too_small' USING ERRCODE = '22023';
  END IF;

  UPDATE battles.templates t
     SET title          = COALESCE(p_title,          t.title),
         description    = COALESCE(p_description,    t.description),
         task_prompt    = COALESCE(p_task_prompt,    t.task_prompt),
         category       = COALESCE(p_category,       t.category),
         max_contenders = COALESCE(p_max_contenders, t.max_contenders),
         is_public      = COALESCE(p_is_public,      t.is_public),
         updated_at     = now()
   WHERE t.id                = p_template_id
     AND t.creator_lenser_id = v_lenser_id
     AND t.deleted_at IS NULL
   RETURNING * INTO v_row;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_owned' USING ERRCODE = '42501';
  END IF;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_battle RECORD;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status <> 'voting' THEN
        RAISE EXCEPTION 'Battle is not in voting phase';
    END IF;

    -- Check voting window
    IF v_battle.voting_closes_at IS NOT NULL AND now() > v_battle.voting_closes_at THEN
        RAISE EXCEPTION 'Voting window has closed';
    END IF;

    -- Prevent contenders from voting on their own battle
    IF EXISTS (
        SELECT 1 FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
          AND c.contender_type = 'human'
          AND c.contender_ref_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'Contenders cannot vote on their own battle';
    END IF;

    -- Delete existing vote if any, then insert new
    DELETE FROM battles.votes
    WHERE battle_id = p_battle_id AND voter_lenser_id = v_lenser_id;

    INSERT INTO battles.votes (battle_id, voter_lenser_id, vote_value, rationale)
    VALUES (p_battle_id, v_lenser_id, p_vote, p_rationale);
END;
$$;


ALTER FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text" DEFAULT NULL::"text", "p_contender_id" "uuid" DEFAULT NULL::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_battle RECORD;
    v_voted_contender_id uuid;
    v_is_draw boolean := false;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status <> 'voting' THEN
        RAISE EXCEPTION 'Battle is not in voting phase';
    END IF;

    -- Check voting window
    IF v_battle.voting_closes_at IS NOT NULL AND now() > v_battle.voting_closes_at THEN
        RAISE EXCEPTION 'Voting window has closed';
    END IF;

    -- Prevent contenders from voting on their own battle
    IF EXISTS (
        SELECT 1 FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
          AND c.contender_ref_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'Contenders cannot vote on their own battle';
    END IF;

    -- Resolve voted_contender_id
    IF p_contender_id IS NOT NULL THEN
        -- Direct contender vote
        IF NOT EXISTS (
            SELECT 1 FROM battles.contenders WHERE id = p_contender_id AND battle_id = p_battle_id
        ) THEN
            RAISE EXCEPTION 'Contender not found in this battle';
        END IF;
        v_voted_contender_id := p_contender_id;
        v_is_draw := false;
    ELSIF p_vote = 'draw' THEN
        v_voted_contender_id := NULL;
        v_is_draw := true;
    ELSE
        -- Legacy slot-based vote: resolve from vote_value
        SELECT c.id INTO v_voted_contender_id
        FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
          AND c.slot = CASE p_vote
              WHEN 'contender_a' THEN 'A'::bpchar
              WHEN 'contender_b' THEN 'B'::bpchar
          END;
        v_is_draw := false;
    END IF;

    -- Insert the vote (no voter_actor_id)
    INSERT INTO battles.votes (
        battle_id, voter_lenser_id, vote_value, rationale,
        voted_contender_id, is_draw, weight, is_ai_vote
    )
    VALUES (
        p_battle_id, v_lenser_id, p_vote, p_rationale,
        v_voted_contender_id, v_is_draw, 1.0, false
    );

    -- Update vote_aggregates
    IF v_voted_contender_id IS NOT NULL AND NOT v_is_draw THEN
        INSERT INTO battles.vote_aggregates (battle_id, contender_id, raw_vote_count, weighted_vote_sum)
        VALUES (p_battle_id, v_voted_contender_id, 1, 1.0)
        ON CONFLICT (battle_id, contender_id) DO UPDATE
        SET raw_vote_count = battles.vote_aggregates.raw_vote_count + 1,
            weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1.0,
            updated_at = now();
    ELSIF v_is_draw THEN
        INSERT INTO battles.vote_aggregates (battle_id, contender_id, draw_count)
        SELECT p_battle_id, c.id, 1
        FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
        ON CONFLICT (battle_id, contender_id) DO UPDATE
        SET draw_count = battles.vote_aggregates.draw_count + 1,
            updated_at = now();
    END IF;

    -- Log the vote event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        p_battle_id,
        'vote_cast',
        v_lenser_id,
        jsonb_build_object(
            'vote_value', p_vote::text,
            'contender_id', v_voted_contender_id,
            'is_draw', v_is_draw
        )
    );
END;
$$;


ALTER FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id uuid;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  IF v_source_id = p_target_profile_id THEN
    RAISE EXCEPTION 'Cannot block yourself';
  END IF;

  -- Upsert block
  INSERT INTO lensers.relationships (source_profile_id, target_profile_id, status)
  VALUES (v_source_id, p_target_profile_id, 'blocked')
  ON CONFLICT (source_profile_id, target_profile_id)
  DO UPDATE SET status = 'blocked', removed_at = now();

  -- Remove their follow of us if it exists
  UPDATE lensers.relationships
  SET status = 'removed', removed_at = now()
  WHERE source_profile_id = p_target_profile_id
    AND target_profile_id = v_source_id
    AND status = 'accepted';

  RETURN jsonb_build_object('blocked', true);
END;
$$;


ALTER FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_browse_battles"("p_category" "text" DEFAULT NULL::"text", "p_status" "text" DEFAULT NULL::"text", "p_q" "text" DEFAULT NULL::"text", "p_after_created" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_after_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "title" "text", "slug" "text", "status" "text", "category" "text", "contender_count" integer, "vote_count" integer, "created_at" timestamp with time zone, "template_title" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
  WITH bounded AS (
    SELECT
      b.id,
      b.title,
      b.slug,
      b.status::text,
      t.category,
      b.total_vote_count,
      b.created_at,
      t.title AS template_title
    FROM battles.battles b
    LEFT JOIN battles.templates t ON t.id = b.template_id
    WHERE b.deleted_at IS NULL
      AND (
        -- Anonymous browse only exposes published / voting battles. Owners
        -- can see all their own via the existing repository methods.
        b.status::text IN ('open', 'voting', 'scoring', 'published')
      )
      AND (p_category IS NULL OR t.category = p_category)
      AND (p_status   IS NULL OR b.status::text = p_status)
      AND (
        p_q IS NULL
        OR to_tsvector('simple', b.title) @@ plainto_tsquery('simple', p_q)
      )
      AND (
        p_after_created IS NULL
        OR (b.created_at, b.id) < (p_after_created, COALESCE(p_after_id, '00000000-0000-0000-0000-000000000000'::uuid))
      )
    ORDER BY b.created_at DESC, b.id DESC
    LIMIT LEAST(GREATEST(p_limit, 1), 100)
  )
  SELECT
    bounded.id,
    bounded.title,
    bounded.slug,
    bounded.status,
    bounded.category,
    COALESCE((SELECT count(*)::int FROM battles.contenders c WHERE c.battle_id = bounded.id), 0)
      AS contender_count,
    COALESCE(bounded.total_vote_count, 0) AS vote_count,
    bounded.created_at,
    bounded.template_title
  FROM bounded
  ORDER BY bounded.created_at DESC, bounded.id DESC;
$$;


ALTER FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") RETURNS TABLE("provider" "text", "key_hint" "text", "label" "text", "is_valid" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'execution', 'agents', 'lensers', 'public'
    AS $$
  SELECT
    bk.provider,
    bk.key_hint,
    bk.label,
    (bk.revoked_at IS NULL AND (bk.expires_at IS NULL OR bk.expires_at > now())) AS is_valid
  FROM execution.byok_keys bk
  WHERE bk.agent_id = p_agent_id
    AND EXISTS (
      SELECT 1 FROM agents.ownerships o
      WHERE o.ai_lenser_id    = p_agent_id
        AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
        AND o.revoked_at IS NULL
    );
$$;


ALTER FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text" DEFAULT NULL::"text", "p_label" "text" DEFAULT NULL::"text", "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_key_id uuid;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships o
    WHERE  o.ai_lenser_id   = p_agent_id
      AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND  o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'byok_key_register_forbidden: agent % not owned by caller', p_agent_id;
  END IF;

  INSERT INTO execution.byok_keys (agent_id, provider, key_encrypted, key_hint, label, expires_at)
  VALUES (p_agent_id, p_provider, p_key_encrypted, p_key_hint, p_label, p_expires_at)
  ON CONFLICT (agent_id, provider) DO UPDATE
    SET key_encrypted = EXCLUDED.key_encrypted,
        key_hint      = EXCLUDED.key_hint,
        label         = EXCLUDED.label,
        expires_at    = EXCLUDED.expires_at,
        revoked_at    = NULL,
        created_at    = now()
  RETURNING id INTO v_key_id;

  RETURN v_key_id;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text" DEFAULT NULL::"text") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row     execution.byok_keys%ROWTYPE;
  v_require boolean;
BEGIN
  SELECT byok_require_reservation INTO v_require FROM billing.runtime_settings WHERE id = 1;
  IF COALESCE(v_require, false) THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_MISSING: legacy fn_byok_key_resolve is disabled; use fn_byok_key_resolve_v2'
      USING ERRCODE = 'P0001';
  END IF;

  SELECT * INTO v_row
  FROM   execution.byok_keys
  WHERE  agent_id   = p_agent_id
    AND  provider   = p_provider
    AND  revoked_at IS NULL
    AND  (expires_at IS NULL OR expires_at > now());

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  IF p_model_id IS NOT NULL
     AND v_row.allowed_model_ids IS NOT NULL
     AND NOT (p_model_id = ANY (v_row.allowed_model_ids)) THEN
    RAISE EXCEPTION 'byok_key_resolve: model % is not in allowed_model_ids for provider %', p_model_id, p_provider
      USING ERRCODE = 'P0001';
  END IF;

  RETURN v_row.key_encrypted;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") IS 'AR / CG1: legacy BYOK resolve. Preserved for backward compatibility with lf-gatewayd <= 0.10. Raises E_BYOK_CONTEXT_MISSING when billing.runtime_settings.byok_require_reservation = true.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'billing', 'agents', 'ai', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_key   execution.byok_keys%ROWTYPE;
  v_res   billing.cost_reservations%ROWTYPE;
  v_model_key text;
BEGIN
  IF p_reservation_id IS NULL THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_MISSING: reservation_id is required'
      USING ERRCODE = 'P0001';
  END IF;

  SELECT * INTO v_res FROM billing.cost_reservations WHERE id = p_reservation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation % not found', p_reservation_id
      USING ERRCODE = 'P0001';
  END IF;

  IF v_res.status <> 'held' THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation % is not held (status=%)',
      p_reservation_id, v_res.status
      USING ERRCODE = 'P0001';
  END IF;
  IF v_res.held_until <= now() THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_EXPIRED: reservation % held_until passed', p_reservation_id
      USING ERRCODE = 'P0001';
  END IF;
  IF v_res.ai_lenser_id <> p_agent_id THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation/agent mismatch'
      USING ERRCODE = 'P0001';
  END IF;
  IF v_res.provider_key <> p_provider THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation/provider mismatch'
      USING ERRCODE = 'P0001';
  END IF;

  -- Optional model binding: if p_model_id is given, it must match the
  -- reservation's model_id (look up via ai.models.key).
  IF p_model_id IS NOT NULL AND LENGTH(p_model_id) > 0 THEN
    SELECT key INTO v_model_key FROM ai.models WHERE id = v_res.model_id;
    IF v_model_key IS DISTINCT FROM p_model_id THEN
      RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation/model mismatch (expected %, got %)',
        v_model_key, p_model_id
        USING ERRCODE = 'P0001';
    END IF;
  END IF;

  SELECT * INTO v_key
  FROM   execution.byok_keys
  WHERE  agent_id   = p_agent_id
    AND  provider   = p_provider
    AND  revoked_at IS NULL
    AND  (expires_at IS NULL OR expires_at > now());
  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  IF p_model_id IS NOT NULL
     AND v_key.allowed_model_ids IS NOT NULL
     AND NOT (p_model_id = ANY (v_key.allowed_model_ids)) THEN
    RAISE EXCEPTION 'byok_key_resolve_v2: model % is not in allowed_model_ids for provider %',
      p_model_id, p_provider
      USING ERRCODE = 'P0001';
  END IF;

  -- Audit row: prefer caller-supplied model id, else fall back to the model
  -- key bound by the reservation. fn_byok_log_usage demands NOT NULL.
  IF p_model_id IS NOT NULL AND LENGTH(p_model_id) > 0 THEN
    PERFORM public.fn_byok_log_usage(v_key.id, NULL, p_model_id, 0);
  ELSIF v_model_key IS NOT NULL THEN
    PERFORM public.fn_byok_log_usage(v_key.id, NULL, v_model_key, 0);
  ELSE
    SELECT m.key INTO v_model_key FROM ai.models m WHERE m.id = v_res.model_id;
    PERFORM public.fn_byok_log_usage(v_key.id, NULL, COALESCE(v_model_key, 'unknown'), 0);
  END IF;

  RETURN v_key.key_encrypted;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") IS 'CG1: BYOK key resolution bound to a held reservation. The reservation_id is the per-call unforgeable token. Service_role only. Logs every resolve to audit.byok_key_usage via fn_byok_log_usage.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'agents', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships o
    WHERE  o.ai_lenser_id   = p_agent_id
      AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND  o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'byok_key_revoke_forbidden';
  END IF;

  UPDATE execution.byok_keys
  SET    revoked_at = now()
  WHERE  agent_id = p_agent_id
    AND  provider  = p_provider;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'audit', 'public'
    AS $$
DECLARE
  v_actor_id UUID;
  v_rows_updated INT;
BEGIN
  v_actor_id := auth.uid();
  IF v_actor_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    JOIN lensers.profiles p ON p.id = al.profile_id
    WHERE al.id = p_agent_id AND p.id = v_actor_id
  ) THEN
    RAISE EXCEPTION 'byok_key_rotate: caller does not own agent %', p_agent_id
      USING ERRCODE = 'P0001';
  END IF;

  UPDATE execution.byok_keys
  SET
    key_encrypted  = p_new_encrypted,
    key_hint       = p_new_hint,
    revoked_at     = NULL,
    created_at     = now(),
    last_rotated_at = now()
  WHERE agent_id = p_agent_id
    AND provider  = p_provider;

  GET DIAGNOSTICS v_rows_updated = ROW_COUNT;
  IF v_rows_updated = 0 THEN
    RAISE EXCEPTION 'byok_key_rotate: no key found for agent % provider %', p_agent_id, p_provider
      USING ERRCODE = 'P0001';
  END IF;

  INSERT INTO audit.events (event_type, actor_type, severity, payload)
  VALUES (
    'byok.key_rotated',
    'user',
    'info',
    jsonb_build_object(
      'agent_id',      p_agent_id,
      'provider',      p_provider,
      'key_hint',      p_new_hint,
      'rotated_by',    v_actor_id,
      'rotated_at',    now()
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") IS 'AR: Rotates an existing BYOK key. Requires the caller to be the agent owner. Atomic UPDATE + audit.events INSERT. SECURITY DEFINER; authenticated role allowed.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  INSERT INTO audit.byok_key_usage (key_id, battle_id, model_id, token_count, caller_role)
  VALUES (p_key_id, p_battle_id, p_model_id, COALESCE(p_token_count, 0), 'service_role');
END;
$$;


ALTER FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) IS 'BZ: Inserts a usage record into audit.byok_key_usage. Called fire-and-forget after each successful model call. SECURITY DEFINER; service_role only. Never exposes key material.';



CREATE TABLE IF NOT EXISTS "execution"."byok_keys" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "agent_id" "uuid" NOT NULL,
    "provider" "text" NOT NULL,
    "key_encrypted" "text" NOT NULL,
    "key_hint" "text",
    "label" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "expires_at" timestamp with time zone,
    "revoked_at" timestamp with time zone,
    "allowed_model_ids" "text"[],
    "last_rotated_at" timestamp with time zone DEFAULT "now"(),
    CONSTRAINT "byok_keys_provider_check" CHECK (("provider" = ANY (ARRAY['openai'::"text", 'anthropic'::"text", 'mistral'::"text", 'google'::"text", 'cohere'::"text", 'custom'::"text"])))
);


ALTER TABLE "execution"."byok_keys" OWNER TO "postgres";


COMMENT ON TABLE "execution"."byok_keys" IS 'Encrypted BYOK API keys for agent-based battle execution. RLS denies all authenticated reads. Accessed only via service_role in trusted workers.';



COMMENT ON COLUMN "execution"."byok_keys"."allowed_model_ids" IS 'AR: Optional allowlist of ai.models.key values this key is permitted to use. NULL means unrestricted. Checked by fn_byok_key_resolve.';



COMMENT ON COLUMN "execution"."byok_keys"."last_rotated_at" IS 'BZ: Timestamp of last key rotation. Used to enforce the 90-day rotation policy. Defaults to now() on INSERT; updated by fn_byok_key_rotate.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_rotation_due"() RETURNS SETOF "execution"."byok_keys"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
  SELECT *
  FROM execution.byok_keys
  WHERE revoked_at IS NULL
    AND last_rotated_at < now() - INTERVAL '90 days'
  ORDER BY last_rotated_at ASC;
$$;


ALTER FUNCTION "public"."fn_byok_rotation_due"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_rotation_due"() IS 'BZ: Returns byok_keys rows where last_rotated_at < now() - 90 days and not revoked. Used by the daily rotation-due cron and lf byok check-rotation CLI command.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_send_rotation_notifications"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'audit', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_key   execution.byok_keys%ROWTYPE;
  v_owner UUID;
  v_count INT := 0;
BEGIN
  FOR v_key IN (
    SELECT * FROM execution.byok_keys
    WHERE revoked_at    IS NULL
      AND last_rotated_at < now() - INTERVAL '90 days'
  ) LOOP
    -- Resolve owner: byok_keys → ai_lensers → profiles
    SELECT lp.id INTO v_owner
    FROM agents.ai_lensers al
    JOIN lensers.profiles lp ON lp.id = al.profile_id
    WHERE al.id = v_key.agent_id
    LIMIT 1;

    IF v_owner IS NOT NULL THEN
      PERFORM public.fn_send_notification(
        v_owner,
        'byok_rotation_due',
        jsonb_build_object(
          'key_id',          v_key.id,
          'provider',        v_key.provider,
          'agent_id',        v_key.agent_id,
          'last_rotated_at', v_key.last_rotated_at,
          'days_overdue',    EXTRACT(DAY FROM now() - v_key.last_rotated_at - INTERVAL '90 days')::int
        )
      );
      v_count := v_count + 1;
    END IF;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_byok_send_rotation_notifications"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_send_rotation_notifications"() IS 'BZ: Sends byok_rotation_due notifications for all overdue keys. Called by daily pg_cron job. Returns count of notifications sent.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_actor_id       UUID;
  v_ai_lenser_id   UUID;
  v_key_row        execution.byok_keys%ROWTYPE;
  v_allowed_models TEXT[];
BEGIN
  -- Must be authenticated
  v_actor_id := auth.uid();
  IF v_actor_id IS NULL THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'authentication_required');
  END IF;

  -- Resolve the ai_lenser_id for this contender
  SELECT cem.ai_lenser_id INTO v_ai_lenser_id
  FROM battles.contender_entity_map cem
  WHERE cem.contender_id = p_contender_id
    AND cem.ai_lenser_id IS NOT NULL
  LIMIT 1;

  IF v_ai_lenser_id IS NULL THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'contender_has_no_agent');
  END IF;

  -- Caller must own this agent
  IF NOT EXISTS (
    SELECT 1
    FROM agents.ai_lensers al
    JOIN lensers.profiles lp ON lp.id = al.profile_id
    WHERE al.id = v_ai_lenser_id
      AND lp.id = v_actor_id
  ) THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'caller_does_not_own_agent');
  END IF;

  -- Get the first valid key for this agent (provider-agnostic validation)
  SELECT * INTO v_key_row
  FROM execution.byok_keys bk
  WHERE bk.agent_id  = v_ai_lenser_id
    AND bk.revoked_at IS NULL
    AND (bk.expires_at IS NULL OR bk.expires_at > now())
  ORDER BY bk.created_at DESC
  LIMIT 1;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'no_valid_key');
  END IF;

  -- Check model allowlist against battle template
  SELECT bt.allowed_model_ids INTO v_allowed_models
  FROM battles.battles b
  JOIN battles.templates bt ON bt.id = b.template_id
  WHERE b.id = p_battle_id;

  IF v_allowed_models IS NOT NULL
    AND array_length(v_allowed_models, 1) > 0
    AND v_key_row.allowed_model_ids IS NOT NULL
  THEN
    -- Verify intersection is non-empty
    IF NOT EXISTS (
      SELECT 1
      FROM unnest(v_key_row.allowed_model_ids) AS km
      WHERE km = ANY(v_allowed_models)
    ) THEN
      RETURN jsonb_build_object(
        'valid', false,
        'reason', 'model_not_in_template_allowlist'
      );
    END IF;
  END IF;

  RETURN jsonb_build_object('valid', true, 'reason', null, 'key_id', v_key_row.id);
END;
$$;


ALTER FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") IS 'BZ: Validates that the contender in a battle has a usable BYOK key. Returns {valid, reason} JSON. SECURITY DEFINER; authenticated owner only.';



CREATE OR REPLACE FUNCTION "public"."fn_cancel_account_deletion_on_login"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_profile RECORD;
  v_event_type text;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RETURN jsonb_build_object('restored', false, 'reason', 'not_authenticated');
  END IF;

  SELECT id, user_id, status, deletion_deadline_at, deletion_requested_at
  INTO v_profile
  FROM lensers.profiles
  WHERE user_id = v_uid
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('restored', false, 'reason', 'no_profile');
  END IF;

  IF v_profile.status = 'pending_deletion'
     AND (v_profile.deletion_deadline_at IS NULL OR v_profile.deletion_deadline_at > now()) THEN
    UPDATE lensers.profiles
    SET status = 'active',
        deletion_requested_at = null,
        deletion_deadline_at = null,
        updated_at = now()
    WHERE id = v_profile.id;

    PERFORM lensers.log_account_lifecycle_event(
      v_profile.id,
      v_profile.user_id,
      'restored_from_pending_deletion',
      v_profile.status,
      'active'::lensers.lenser_status,
      'signin_restore',
      jsonb_build_object(
        'restored_before_deadline', true,
        'deletion_deadline_at', v_profile.deletion_deadline_at
      )
    );

    RETURN jsonb_build_object('restored', true, 'from_status', 'pending_deletion');
  END IF;

  IF (
    v_profile.status = 'deactivated' OR
    (v_profile.status = 'active' AND v_profile.deletion_requested_at IS NOT NULL)
  ) AND (
    v_profile.deletion_deadline_at IS NULL OR v_profile.deletion_deadline_at > now()
  ) THEN
    UPDATE lensers.profiles
    SET status = 'active',
        deletion_requested_at = null,
        deletion_deadline_at = null,
        updated_at = now()
    WHERE id = v_profile.id;

    v_event_type := CASE
      WHEN v_profile.status = 'active' THEN 'restored_from_pending_deletion'
      ELSE 'restored_from_deactivated'
    END;

    PERFORM lensers.log_account_lifecycle_event(
      v_profile.id,
      v_profile.user_id,
      v_event_type,
      v_profile.status,
      'active'::lensers.lenser_status,
      'signin_restore',
      jsonb_build_object(
        'restored_before_deadline', true,
        'deletion_deadline_at', v_profile.deletion_deadline_at,
        'cleared_stale_deletion_flags', v_profile.status = 'active'
      )
    );

    RETURN jsonb_build_object('restored', true, 'from_status', v_profile.status);
  END IF;

  RETURN jsonb_build_object('restored', false, 'reason', 'no_action_needed');
END;
$$;


ALTER FUNCTION "public"."fn_cancel_account_deletion_on_login"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
BEGIN
  UPDATE agents.team_runs
  SET status = 'cancelled'
  WHERE id = p_team_run_id
    AND ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = v_lenser_id
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    );
END;
$$;


ALTER FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") RETURNS TABLE("cancelled_run_count" integer, "cancelled_job_count" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'battles', 'public', 'lensers'
    AS $$
DECLARE
  v_profile_id UUID;
  v_run_count  INT;
  v_job_count  INT;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'cancel_all_active_runs_forbidden: admin role required';
  END IF;

  SELECT id INTO v_profile_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  -- 1. Cancel all active / queued workflow runs and cascade to node results in
  --    one atomic CTE. The RETURNING ids are used directly in the cascade UPDATE
  --    to avoid any time-window ambiguity.
  WITH cancelled_runs AS (
    UPDATE lenses.workflow_runs
    SET    status       = 'cancelled',
           completed_at = COALESCE(completed_at, now())
    WHERE  status IN ('running', 'streaming', 'recovered', 'queued', 'pending')
    RETURNING id
  ),
  node_cascade AS (
    UPDATE lenses.workflow_node_results nr
    SET    status        = 'cancelled',
           completed_at  = COALESCE(nr.completed_at, now()),
           error_message = COALESCE(nr.error_message, p_reason)
    FROM   cancelled_runs cr
    WHERE  nr.run_id = cr.id
      AND  nr.status NOT IN (
             'completed', 'failed', 'cancelled',
             'timed_out', 'skipped', 'blocked', 'invalidated'
           )
    RETURNING 1
  )
  SELECT COUNT(DISTINCT cr.id)::INT INTO v_run_count
  FROM   cancelled_runs cr;

  -- 2. Cancel all active / queued battle execution jobs.
  WITH cancelled_jobs AS (
    UPDATE battles.battle_execution_jobs
    SET    status       = 'failed',
           completed_at = now(),
           error_message = COALESCE(error_message, p_reason)
    WHERE  status IN ('queued', 'claimed', 'running')
    RETURNING id
  )
  SELECT COUNT(*)::INT INTO v_job_count FROM cancelled_jobs;

  -- 3. Audit trail.
  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'emergency.cancel_all',
    'lenser',
    v_profile_id,
    'warn',
    jsonb_build_object(
      'reason',          p_reason,
      'cancelled_runs',  v_run_count,
      'cancelled_jobs',  v_job_count
    )
  );

  RETURN QUERY SELECT v_run_count, v_job_count;
END;
$$;


ALTER FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") IS 'Admin-only: atomically cancel ALL active workflow runs (and their node results) plus all active battle execution jobs. Returns (cancelled_run_count, cancelled_job_count). Uses a CTE to keep run and node-result cancellation in one transaction.';



CREATE OR REPLACE FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run agents.team_runs;
BEGIN
  SELECT * INTO v_run
    FROM agents.team_runs
   WHERE id = p_team_run_id
   FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'team run not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_run.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF v_run.status IN ('completed','failed','cancelled') THEN
    RAISE EXCEPTION 'run already terminal' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.team_runs
     SET status       = 'cancelled',
         completed_at = now()
   WHERE id = p_team_run_id;

  IF v_run.workflow_run_id IS NOT NULL THEN
    UPDATE lenses.workflow_runs
       SET status       = 'cancelled',
           completed_at = COALESCE(completed_at, now())
     WHERE id = v_run.workflow_run_id
       AND status NOT IN ('completed','failed','cancelled');
  END IF;

  INSERT INTO agents.agent_run_events (
    team_run_id,
    event_type,
    payload,
    occurred_at
  ) VALUES (
    p_team_run_id,
    'run_cancelled',
    jsonb_build_object(
      'cancelled_by',     auth.uid(),
      'previous_status',  v_run.status
    ),
    now()
  );

  INSERT INTO agents.policy_evaluations (
    ai_lenser_id,
    team_run_id,
    evaluation_point,
    policy_type,
    verdict,
    reason,
    context,
    evaluated_at
  ) VALUES (
    v_run.ai_lenser_id,
    p_team_run_id,
    'pre_run',
    'kill_switch',
    'deny',
    'run cancelled by owner',
    jsonb_build_object('cancelled_by', auth.uid()),
    now()
  );
END;
$$;


ALTER FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer DEFAULT 0) RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_run            lenses.workflow_runs%ROWTYPE;
  v_projected      integer;
  v_pending        integer := GREATEST(COALESCE(p_pending_credits, 0), 0);
  v_cost_metadata  jsonb;
BEGIN
  SELECT * INTO v_run
  FROM lenses.workflow_runs
  WHERE id = p_run_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN FALSE;
  END IF;

  -- Never cancel already-terminal runs.
  IF v_run.status IN ('completed', 'failed', 'cancelled', 'timed_out') THEN
    RETURN FALSE;
  END IF;

  IF v_run.budget_credits IS NULL THEN
    RETURN FALSE;
  END IF;

  v_projected := COALESCE(v_run.spent_credits, 0) + v_pending;

  IF v_projected <= v_run.budget_credits THEN
    RETURN FALSE;
  END IF;

  v_cost_metadata := COALESCE(v_run.cost_metadata, '{}'::jsonb)
    || jsonb_build_object(
      'budgetCancellation', jsonb_build_object(
        'cancelledAt', now(),
        'budgetCredits', v_run.budget_credits,
        'spentCredits', v_run.spent_credits,
        'pendingCredits', v_pending,
        'projectedCredits', v_projected,
        'reason', 'budget_exceeded'
      )
    );

  UPDATE lenses.workflow_runs
  SET
    status       = 'cancelled',
    completed_at = now(),
    run_worker_id = NULL,
    cost_metadata = v_cost_metadata
  WHERE id = p_run_id;

  -- Cascade cancellation to any still-running node results so the timeline
  -- view does not keep spinners going forever.
  UPDATE lenses.workflow_node_results
  SET status       = 'cancelled',
      completed_at = now(),
      error_message = COALESCE(error_message, 'budget_exceeded')
  WHERE run_id = p_run_id
    AND status IN ('pending', 'awaiting_dependency', 'queued', 'running', 'streaming', 'retrying');

  -- Append an event so SSE clients see the cancellation immediately.
  INSERT INTO lenses.workflow_run_events (run_id, event_id, type, payload)
  SELECT
    p_run_id,
    COALESCE(MAX(e.event_id), 0) + 1,
    'run.cancelled',
    jsonb_build_object(
      'runId', p_run_id,
      'reason', 'budget_exceeded',
      'budgetCredits', v_run.budget_credits,
      'spentCredits', v_run.spent_credits,
      'pendingCredits', v_pending
    )
  FROM lenses.workflow_run_events e
  WHERE e.run_id = p_run_id;

  RETURN TRUE;
END;
$$;


ALTER FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) IS 'Phase 9 — cancels a run when spent_credits + pendingReservations exceed budget_credits. Cascades to node results and emits run.cancelled event. Invoked periodically by the worker heartbeat loop.';



CREATE OR REPLACE FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_agg public.notification_aggregates;
BEGIN
  -- Look for an open window; lock the row to prevent parallel duplicates.
  SELECT *
  INTO   v_agg
  FROM   public.notification_aggregates
  WHERE  recipient_id      = p_recipient_id
    AND  notification_type = p_type
    AND  entity_id         = p_entity_id
    AND  window_end        > now()
  LIMIT  1
  FOR UPDATE SKIP LOCKED;

  IF FOUND THEN
    -- Within the window: append actor (if new) and suppress new notification.
    IF NOT (p_actor_id = ANY(v_agg.actor_ids)) THEN
      UPDATE public.notification_aggregates
      SET    actor_ids = array_append(actor_ids, p_actor_id)
      WHERE  id        = v_agg.id;
    END IF;
    RETURN false;
  END IF;

  -- No open window: create a new one and allow the notification.
  INSERT INTO public.notification_aggregates
    (recipient_id, notification_type, entity_id, window_start, window_end, actor_ids)
  VALUES
    (p_recipient_id, p_type, p_entity_id, now(), now() + p_window, ARRAY[p_actor_id]);

  RETURN true;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_check_and_upsert_aggregate failed: %', SQLERRM;
  RETURN true;  -- fail-open — allow the notification on error
END;
$$;


ALTER FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_handle"("p_handle" "text") RETURNS TABLE("verdict" "text", "class_hit" "text", "risk_score" smallint, "reason_codes" "text"[], "is_available" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT
    v.verdict,
    v.class_hit,
    v.risk_score,
    v.reason_codes,
    (v.verdict = 'allow') AS is_available
  FROM identity_gov.fn_validate_handle(p_handle) v;
$$;


ALTER FUNCTION "public"."fn_check_handle"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_check_handle"("p_handle" "text") IS 'Public read-through handle availability check. Safe for debounced UI calls (STABLE, no writes). Granted to authenticated and anon; results are advisory — fn_claim_handle on the write path performs the authoritative check.';



CREATE TABLE IF NOT EXISTS "battles"."media_quality_results" (
    "submission_id" "uuid" NOT NULL,
    "passed" boolean NOT NULL,
    "violations" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "checked_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "battles"."media_quality_results" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") RETURNS "battles"."media_quality_results"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_submission  RECORD;
  v_template_id UUID;
  v_modality    TEXT;
  v_meta        JSONB;
  v_rule        RECORD;
  v_violations  TEXT[] := ARRAY[]::TEXT[];
  v_width       INT;
  v_height      INT;
  v_duration    INT;
  v_aspect      TEXT;
  v_size_mb     NUMERIC;
  v_result      battles.media_quality_results%ROWTYPE;
BEGIN
  SELECT s.*, b.template_id AS battle_template_id
    INTO v_submission
    FROM battles.submissions s
    JOIN battles.battles b ON b.id = s.battle_id
   WHERE s.id = p_submission_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'submission_not_found' USING ERRCODE = '42501';
  END IF;

  v_template_id := v_submission.battle_template_id;
  v_meta        := COALESCE(v_submission.metadata->'media', '{}'::jsonb);
  v_modality    := COALESCE(v_meta->>'modality', v_submission.metadata->>'output_modality');

  IF v_modality IS NULL OR v_modality NOT IN ('image','video','audio') THEN
    RAISE EXCEPTION 'unknown_modality: %', COALESCE(v_modality, '<null>')
      USING ERRCODE = '22023';
  END IF;

  -- Lookup the rule. Absence of a rule = pass (no floor declared).
  -- NOTE: a plpgsql record-typed `v_rule IS NOT NULL` is only true when EVERY
  -- column is non-NULL — wrong test for a row with optional fields. Use FOUND
  -- so we only enforce when a row was actually selected.
  SELECT * INTO v_rule
    FROM battles.media_quality_rules
   WHERE template_id = v_template_id
     AND modality    = v_modality;

  IF FOUND THEN
    v_width    := NULLIF(v_meta->>'width', '')::INT;
    v_height   := NULLIF(v_meta->>'height', '')::INT;
    v_duration := NULLIF(v_meta->>'duration_seconds', '')::INT;
    v_aspect   := v_meta->>'aspect_ratio';
    v_size_mb  := NULLIF(v_meta->>'size_mb', '')::NUMERIC;

    IF v_rule.min_width IS NOT NULL
       AND (v_width IS NULL OR v_width < v_rule.min_width) THEN
      v_violations := array_append(v_violations,
        format('min_width:%s<%s', COALESCE(v_width::text, 'null'), v_rule.min_width));
    END IF;
    IF v_rule.min_height IS NOT NULL
       AND (v_height IS NULL OR v_height < v_rule.min_height) THEN
      v_violations := array_append(v_violations,
        format('min_height:%s<%s', COALESCE(v_height::text, 'null'), v_rule.min_height));
    END IF;
    IF v_rule.max_duration_seconds IS NOT NULL
       AND v_duration IS NOT NULL
       AND v_duration > v_rule.max_duration_seconds THEN
      v_violations := array_append(v_violations,
        format('max_duration_seconds:%s>%s', v_duration, v_rule.max_duration_seconds));
    END IF;
    IF v_rule.required_aspect_ratio IS NOT NULL
       AND v_aspect IS DISTINCT FROM v_rule.required_aspect_ratio THEN
      v_violations := array_append(v_violations,
        format('aspect_ratio:%s!=%s', COALESCE(v_aspect, 'null'), v_rule.required_aspect_ratio));
    END IF;
    IF v_rule.max_file_size_mb IS NOT NULL
       AND v_size_mb IS NOT NULL
       AND v_size_mb > v_rule.max_file_size_mb THEN
      v_violations := array_append(v_violations,
        format('max_file_size_mb:%s>%s', v_size_mb, v_rule.max_file_size_mb));
    END IF;
  END IF;

  INSERT INTO battles.media_quality_results (
    submission_id, passed, violations, checked_at
  ) VALUES (
    p_submission_id,
    cardinality(v_violations) = 0,
    to_jsonb(v_violations),
    now()
  )
  ON CONFLICT (submission_id) DO UPDATE SET
    passed     = EXCLUDED.passed,
    violations = EXCLUDED.violations,
    checked_at = EXCLUDED.checked_at
  RETURNING * INTO v_result;

  RETURN v_result;
END $$;


ALTER FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  v_recent_count INT;
BEGIN
  SELECT COUNT(*)
    INTO v_recent_count
    FROM battles.votes
   WHERE voter_lenser_id = p_voter_id
     AND created_at >= now() - INTERVAL '60 seconds';

  IF v_recent_count >= 3 THEN
    RAISE EXCEPTION 'rate_limit_exceeded'
      USING ERRCODE = 'P0001',
            DETAIL  = 'You are voting too fast. Please wait before casting another vote.',
            HINT    = 'rate_limit_exceeded';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") IS 'CA: Raises P0001 rate_limit_exceeded when voter has cast ≥3 votes in the last 60s.';



CREATE OR REPLACE FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_eligibility text;
  v_type        text;
  v_onboarding  text;
BEGIN
  SELECT voter_eligibility INTO v_eligibility
  FROM battles.battles
  WHERE id = p_battle_id;

  IF NOT FOUND THEN RETURN false; END IF;
  IF v_eligibility = 'open' THEN RETURN true; END IF;

  SELECT type, onboarding_step INTO v_type, v_onboarding
  FROM lensers.profiles
  WHERE id = p_lenser_id;

  IF NOT FOUND THEN RETURN false; END IF;

  IF    v_eligibility = 'human_only'      THEN RETURN v_type = 'human';
  ELSIF v_eligibility = 'ai_only'         THEN RETURN v_type = 'ai';
  ELSIF v_eligibility = 'verified_lenser' THEN RETURN v_onboarding = 'completed';
  END IF;

  RETURN true;
END;
$$;


ALTER FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_webhook_signing_secret"() RETURNS TABLE("is_set" boolean, "length_bytes" integer, "strict_mode" boolean)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  v_secret text := current_setting('app.webhook_signing_secret', true);
  v_strict text := current_setting('app.webhook_strict_signing', true);
BEGIN
  is_set := v_secret IS NOT NULL AND length(v_secret) > 0;
  length_bytes := COALESCE(length(v_secret), 0);
  strict_mode := COALESCE(lower(v_strict) = 'true', false);
  RETURN NEXT;
END;
$$;


ALTER FUNCTION "public"."fn_check_webhook_signing_secret"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer DEFAULT 60000, "p_max_claims" integer DEFAULT 1) RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "parent_run_id" "uuid", "recursion_depth" integer, "previous_status" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_threshold interval := make_interval(secs => GREATEST(p_stale_after_ms, 0)::numeric / 1000.0);
BEGIN
  IF p_worker_id IS NULL OR length(p_worker_id) = 0 THEN
    RAISE EXCEPTION 'fn_claim_stale_workflow_run: p_worker_id is required'
      USING ERRCODE = '22023';
  END IF;

  RETURN QUERY
  WITH claimable AS (
    SELECT r.id, r.workflow_id, r.parent_run_id, r.recursion_depth, r.status
    FROM lenses.workflow_runs r
    WHERE r.status IN ('queued', 'running', 'streaming', 'recovered')
      AND (
            r.heartbeat_at IS NULL
        OR  r.heartbeat_at < (now() - v_threshold)
      )
      -- If another worker already owns the run AND is heartbeating, we
      -- would have filtered it above. The remaining `run_worker_id IS NOT
      -- NULL` rows are genuinely stale.
    ORDER BY COALESCE(r.heartbeat_at, r.started_at, r.created_at) ASC
    LIMIT GREATEST(p_max_claims, 1)
    FOR UPDATE SKIP LOCKED
  ),
  claimed AS (
    UPDATE lenses.workflow_runs r
    SET status        = 'recovered',
        run_worker_id = p_worker_id,
        heartbeat_at  = now()
    FROM claimable c
    WHERE r.id = c.id
    RETURNING r.id, r.workflow_id, r.parent_run_id, r.recursion_depth, c.status AS previous_status
  )
  SELECT
    claimed.id,
    claimed.workflow_id,
    claimed.parent_run_id,
    claimed.recursion_depth,
    claimed.previous_status
  FROM claimed;
END;
$$;


ALTER FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) IS 'Phase 7 — Atomic claim of stale workflow runs for the crash-recovery loop. Uses FOR UPDATE SKIP LOCKED; multiple workers can race safely. Sets status=recovered and stamps run_worker_id + heartbeat_at in one transaction. Returns zero rows when nothing is claimable.';



CREATE OR REPLACE FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers', 'content'
    AS $$
  SELECT lenses.fn_clone_lens(p_source_lens_id, p_version_id);
$$;


ALTER FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_caller_id uuid;
  v_source    lenses.workflows%ROWTYPE;
  v_new_id    uuid := gen_random_uuid();
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT wf.* INTO v_source
  FROM lenses.workflows wf
  WHERE wf.id = p_source_workflow_id;

  IF NOT FOUND OR v_source.visibility::text != 'public' THEN
    RAISE EXCEPTION 'Workflow not found or not public';
  END IF;

  -- Create new workflow (private, owned by caller)
  INSERT INTO lenses.workflows (id, lenser_id, title, description, visibility, battle_count, fork_count)
  VALUES (
    v_new_id,
    v_caller_id,
    'Fork of ' || v_source.title,
    v_source.description,
    'private',
    0,
    0
  );

  UPDATE lenses.workflows wf2
  SET parent_workflow_id = p_source_workflow_id
  WHERE wf2.id = v_new_id;

  -- Copy nodes with config, building old->new id map
  CREATE TEMP TABLE _clone_node_map (old_id uuid, new_id uuid) ON COMMIT DROP;

  INSERT INTO _clone_node_map (old_id, new_id)
  SELECT n.id, gen_random_uuid()
  FROM lenses.workflow_nodes n
  WHERE n.workflow_id = p_source_workflow_id;

  INSERT INTO lenses.workflow_nodes (id, workflow_id, lens_id, version_id, position_x, position_y, label, ordinal, config)
  SELECT m.new_id, v_new_id, n.lens_id, n.version_id, n.position_x, n.position_y, n.label, n.ordinal, n.config
  FROM lenses.workflow_nodes n
  JOIN _clone_node_map m ON m.old_id = n.id;

  -- Copy edges, translating old node ids to new ids
  INSERT INTO lenses.workflow_edges (id, workflow_id, source_node_id, target_node_id, source_output_key, target_param_label)
  SELECT
    gen_random_uuid(),
    v_new_id,
    ms.new_id,
    mt.new_id,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  JOIN _clone_node_map ms ON ms.old_id = e.source_node_id
  JOIN _clone_node_map mt ON mt.old_id = e.target_node_id
  WHERE e.workflow_id = p_source_workflow_id;

  -- Increment fork_count on source
  UPDATE lenses.workflows src
  SET fork_count = src.fork_count + 1
  WHERE src.id = p_source_workflow_id;

  -- Award XP (best-effort)
  BEGIN
    PERFORM xp.apply(
      v_caller_id,
      'WORKFLOW_FORKED',
      'content'::xp.source_enum,
      'workflow',
      v_new_id
    );
    PERFORM xp.apply(
      v_source.lenser_id,
      'WORKFLOW_FORK_RECEIVED',
      'content'::xp.source_enum,
      'workflow',
      p_source_workflow_id
    );
  EXCEPTION WHEN OTHERS THEN
    NULL;
  END;

  RETURN QUERY
    SELECT
      w.id, w.lenser_id, w.title, w.description, w.visibility::text,
      w.battle_count, w.reaction_totals, w.fork_count, w.parent_workflow_id,
      w.created_at, w.updated_at
    FROM lenses.workflows w
    WHERE w.id = v_new_id;
END;
$$;


ALTER FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") IS 'Clones a public workflow including nodes (with config), edges, and XP awards. Returns the new workflow row.';



CREATE OR REPLACE FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_existing_profile_id UUID;
BEGIN
  -- Confirm the caller owns a profile
  SELECT id INTO v_existing_profile_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_existing_profile_id IS NULL THEN
    RAISE EXCEPTION 'PROFILE_NOT_FOUND';
  END IF;

  -- Guard handle uniqueness (raise on conflict so UI can show a clear error)
  IF EXISTS (
    SELECT 1 FROM lensers.profiles
    WHERE handle = lower(p_handle)
      AND id <> v_existing_profile_id
  ) THEN
    RAISE EXCEPTION 'HANDLE_TAKEN';
  END IF;

  UPDATE lensers.profiles
  SET
    handle              = lower(p_handle),
    display_name        = p_display_name,
    onboarding_step     = 2,
    onboarding_completed_at = now(),
    updated_at          = now()
  WHERE id = v_existing_profile_id;
END;
$$;


ALTER FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."scratchpad_runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "actor_lenser_id" "uuid" NOT NULL,
    "prompt" "text" NOT NULL,
    "model_id" "uuid",
    "tool_calls" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "output" "text",
    "status" "text" DEFAULT 'queued'::"text" NOT NULL,
    "error" "text",
    "cost_credits" integer DEFAULT 0 NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "completed_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "scratchpad_runs_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'running'::"text", 'completed'::"text", 'failed'::"text", 'cancelled'::"text"])))
);


ALTER TABLE "agents"."scratchpad_runs" OWNER TO "postgres";


COMMENT ON TABLE "agents"."scratchpad_runs" IS 'One-off owner-driven scratchpad runs. Owner-only via RLS. Promote outputs into memory_profiles via fn_promote_scratchpad_to_memory.';



CREATE OR REPLACE FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text" DEFAULT 'completed'::"text", "p_cost_credits" integer DEFAULT 0, "p_error" "text" DEFAULT NULL::"text") RETURNS "agents"."scratchpad_runs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_existing agents.scratchpad_runs;
  v_row      agents.scratchpad_runs;
BEGIN
  SELECT * INTO v_existing FROM agents.scratchpad_runs WHERE id = p_run_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'scratchpad run not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_existing.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF p_status NOT IN ('running', 'completed', 'failed', 'cancelled') THEN
    RAISE EXCEPTION 'invalid status' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.scratchpad_runs
  SET output       = p_output,
      status       = p_status,
      cost_credits = COALESCE(p_cost_credits, 0),
      error        = p_error,
      completed_at = CASE
                       WHEN p_status IN ('completed', 'failed', 'cancelled') THEN now()
                       ELSE completed_at
                     END
  WHERE id = p_run_id
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb" DEFAULT NULL::"jsonb", "p_error" "text" DEFAULT NULL::"text", "p_cost" numeric DEFAULT NULL::numeric) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_inv agents.tool_invocations;
BEGIN
  SELECT * INTO v_inv FROM agents.tool_invocations WHERE id = p_invocation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'invocation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_inv.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF p_status NOT IN ('completed','failed') THEN
    RAISE EXCEPTION 'completion status must be completed or failed' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.tool_invocations
     SET status        = p_status,
         output        = COALESCE(p_output, output),
         error         = COALESCE(p_error, error),
         cost_estimate = COALESCE(p_cost, cost_estimate),
         completed_at  = now()
   WHERE id = p_invocation_id;
END;
$$;


ALTER FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'reputation'
    AS $$
DECLARE
  v_battle           battles.battles%ROWTYPE;
  v_winner_lenser_id uuid;
  v_loser_lenser_id  uuid;
  v_winner_score     numeric(10,4);
  v_loser_score      numeric(10,4);
  v_winner_new       numeric(10,4);
  v_loser_new        numeric(10,4);
  v_e_winner         numeric;
  v_e_loser          numeric;
  v_k_factor         constant integer := 32;
  v_default_score    constant numeric := 1000;
  v_is_draw          boolean := false;
BEGIN
  -- Idempotency guard: insert log row first; bail if a row already exists.
  INSERT INTO reputation.elo_battle_log (battle_id, k_factor)
  VALUES (p_battle_id, v_k_factor)
  ON CONFLICT (battle_id) DO NOTHING;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('skipped', true, 'reason', 'already_computed', 'battle_id', p_battle_id);
  END IF;

  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    -- Battle vanished after we logged; clean up to allow a future retry.
    DELETE FROM reputation.elo_battle_log WHERE battle_id = p_battle_id;
    RAISE EXCEPTION 'Battle % not found', p_battle_id USING ERRCODE = '42704';
  END IF;

  IF v_battle.status <> 'closed' THEN
    DELETE FROM reputation.elo_battle_log WHERE battle_id = p_battle_id;
    RAISE EXCEPTION 'Battle % is in status %, must be closed before ELO compute',
      p_battle_id, v_battle.status USING ERRCODE = '22023';
  END IF;

  -- Resolve winner/loser. NULL winner_contender_id ⇒ tie.
  IF v_battle.winner_contender_id IS NULL THEN
    v_is_draw := true;
    SELECT contender_ref_id
      INTO v_winner_lenser_id
    FROM battles.contenders
    WHERE battle_id = p_battle_id AND slot = 'A'
    LIMIT 1;

    SELECT contender_ref_id
      INTO v_loser_lenser_id
    FROM battles.contenders
    WHERE battle_id = p_battle_id AND slot = 'B'
    LIMIT 1;
  ELSE
    SELECT contender_ref_id
      INTO v_winner_lenser_id
    FROM battles.contenders
    WHERE id = v_battle.winner_contender_id;

    SELECT contender_ref_id
      INTO v_loser_lenser_id
    FROM battles.contenders
    WHERE battle_id = p_battle_id
      AND id <> v_battle.winner_contender_id
    LIMIT 1;
  END IF;

  IF v_winner_lenser_id IS NULL OR v_loser_lenser_id IS NULL THEN
    DELETE FROM reputation.elo_battle_log WHERE battle_id = p_battle_id;
    RAISE EXCEPTION 'Battle % has insufficient contenders for ELO compute', p_battle_id
      USING ERRCODE = '22023';
  END IF;

  -- Read current ELO scores; default 1000 if no row.
  SELECT score INTO v_winner_score
    FROM reputation.lenser_scores
   WHERE lenser_id = v_winner_lenser_id AND score_type = 'elo';
  v_winner_score := COALESCE(v_winner_score, v_default_score);

  SELECT score INTO v_loser_score
    FROM reputation.lenser_scores
   WHERE lenser_id = v_loser_lenser_id AND score_type = 'elo';
  v_loser_score := COALESCE(v_loser_score, v_default_score);

  -- Expected scores.
  v_e_winner := 1.0 / (1.0 + power(10.0, (v_loser_score - v_winner_score) / 400.0));
  v_e_loser  := 1.0 / (1.0 + power(10.0, (v_winner_score - v_loser_score) / 400.0));

  IF v_is_draw THEN
    v_winner_new := v_winner_score + v_k_factor * (0.5 - v_e_winner);
    v_loser_new  := v_loser_score  + v_k_factor * (0.5 - v_e_loser);
  ELSE
    v_winner_new := v_winner_score + v_k_factor * (1.0 - v_e_winner);
    v_loser_new  := v_loser_score  + v_k_factor * (0.0 - v_e_loser);
  END IF;

  -- Persist updated scores.
  INSERT INTO reputation.lenser_scores (lenser_id, score_type, score, computed_at)
  VALUES (v_winner_lenser_id, 'elo', v_winner_new, now())
  ON CONFLICT (lenser_id, score_type) DO UPDATE
    SET score = EXCLUDED.score, computed_at = now();

  INSERT INTO reputation.lenser_scores (lenser_id, score_type, score, computed_at)
  VALUES (v_loser_lenser_id, 'elo', v_loser_new, now())
  ON CONFLICT (lenser_id, score_type) DO UPDATE
    SET score = EXCLUDED.score, computed_at = now();

  -- Backfill the audit log row inserted at top of function.
  UPDATE reputation.elo_battle_log
  SET winner_lenser_id    = v_winner_lenser_id,
      loser_lenser_id     = v_loser_lenser_id,
      is_draw             = v_is_draw,
      winner_score_before = v_winner_score,
      winner_score_after  = v_winner_new,
      loser_score_before  = v_loser_score,
      loser_score_after   = v_loser_new
  WHERE battle_id = p_battle_id;

  RETURN jsonb_build_object(
    'battle_id',           p_battle_id,
    'is_draw',             v_is_draw,
    'winner_lenser_id',    v_winner_lenser_id,
    'loser_lenser_id',     v_loser_lenser_id,
    'winner_score_before', v_winner_score,
    'winner_score_after',  v_winner_new,
    'loser_score_before',  v_loser_score,
    'loser_score_after',   v_loser_new,
    'k_factor',            v_k_factor
  );
END;
$$;


ALTER FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") IS 'Phase O3: K=32 per-battle ELO update. Reads winner/loser from battles.contenders, applies the standard formula, upserts reputation.lenser_scores (score_type=elo), and logs the operation in reputation.elo_battle_log for idempotency.';



CREATE OR REPLACE FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) RETURNS "jsonb"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'public', 'connectors'
    AS $$
BEGIN
    PERFORM connectors.fn_assert_scope(p_granted, p_required);
    RETURN jsonb_build_object('ok', true, 'required', p_required);
END
$$;


ALTER FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_workspace_id uuid := connectors.fn_active_workspace_id();
    v_user_id      uuid := auth.uid();
    v_connector_id uuid;
    v_token_raw    text;
    v_token_hash   text;
    v_token_prefix text;
BEGIN
    IF v_workspace_id IS NULL THEN
        RAISE EXCEPTION 'no active workspace for caller'
              USING ERRCODE = '42501';
    END IF;

    PERFORM connectors.fn_assert_known_scopes(p_scopes);
    IF cardinality(p_scopes) = 0 THEN
        RAISE EXCEPTION 'p_scopes must be non-empty' USING ERRCODE = '22023';
    END IF;

    INSERT INTO connectors.connectors (workspace_id, slug, name, description, created_by)
    VALUES (v_workspace_id, p_slug, p_name, NULLIF(p_description, ''), v_user_id)
    RETURNING id INTO v_connector_id;

    -- 32 random bytes → ~43-char base64url; "lf_" prefix marks origin.
    v_token_raw    := 'lf_' || encode(gen_random_bytes(32), 'base64');
    v_token_raw    := replace(replace(replace(v_token_raw, '+', '-'), '/', '_'), '=', '');
    v_token_hash   := connectors.fn_hash_token(v_token_raw);
    v_token_prefix := substring(v_token_raw FROM 1 FOR 11);

    INSERT INTO connectors.connector_tokens (connector_id, token_hash, token_prefix, scopes)
    VALUES (v_connector_id, v_token_hash, v_token_prefix, p_scopes);

    RETURN jsonb_build_object(
        'slug',          p_slug,
        'name',          p_name,
        'scopes',        p_scopes,
        'service_token', v_token_raw,
        'token_prefix',  v_token_prefix
    );
END
$$;


ALTER FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_get"("p_slug" "text") RETURNS TABLE("slug" "text", "name" "text", "description" "text", "scopes" "text"[], "is_active" boolean, "kind" "text", "created_at" timestamp with time zone, "last_used_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
    SELECT c.slug,
           c.name,
           c.description,
           COALESCE(
               (SELECT t.scopes
                  FROM connectors.connector_tokens t
                 WHERE t.connector_id = c.id
                   AND t.revoked_at IS NULL
                 ORDER BY t.created_at DESC
                 LIMIT 1),
               ARRAY[]::text[]
           ) AS scopes,
           c.is_active,
           c.kind,
           c.created_at,
           c.last_used_at
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug
     LIMIT 1
$$;


ALTER FUNCTION "public"."fn_connector_get"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_remove"("p_slug" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_connector_id uuid;
BEGIN
    SELECT c.id INTO v_connector_id
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug;

    IF v_connector_id IS NULL THEN
        RAISE EXCEPTION 'connector not found: %', p_slug USING ERRCODE = 'P0002';
    END IF;

    UPDATE connectors.connectors
       SET is_active = false
     WHERE id = v_connector_id;

    UPDATE connectors.connector_tokens
       SET revoked_at = now()
     WHERE connector_id = v_connector_id
       AND revoked_at IS NULL;
END
$$;


ALTER FUNCTION "public"."fn_connector_remove"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_rotate"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_connector_id uuid;
    v_scopes       text[];
    v_token_raw    text;
    v_token_hash   text;
    v_token_prefix text;
BEGIN
    SELECT c.id INTO v_connector_id
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug
       AND c.is_active = true;

    IF v_connector_id IS NULL THEN
        RAISE EXCEPTION 'connector not found or inactive: %', p_slug USING ERRCODE = 'P0002';
    END IF;

    SELECT t.scopes INTO v_scopes
      FROM connectors.connector_tokens t
     WHERE t.connector_id = v_connector_id
       AND t.revoked_at IS NULL
     ORDER BY t.created_at DESC
     LIMIT 1;

    IF v_scopes IS NULL THEN
        RAISE EXCEPTION 'no active token to rotate for connector: %', p_slug USING ERRCODE = 'P0002';
    END IF;

    UPDATE connectors.connector_tokens
       SET revoked_at = now()
     WHERE connector_id = v_connector_id
       AND revoked_at IS NULL;

    v_token_raw    := 'lf_' || encode(gen_random_bytes(32), 'base64');
    v_token_raw    := replace(replace(replace(v_token_raw, '+', '-'), '/', '_'), '=', '');
    v_token_hash   := connectors.fn_hash_token(v_token_raw);
    v_token_prefix := substring(v_token_raw FROM 1 FOR 11);

    INSERT INTO connectors.connector_tokens (connector_id, token_hash, token_prefix, scopes)
    VALUES (v_connector_id, v_token_hash, v_token_prefix, v_scopes);

    RETURN jsonb_build_object(
        'slug',          p_slug,
        'service_token', v_token_raw,
        'token_prefix',  v_token_prefix
    );
END
$$;


ALTER FUNCTION "public"."fn_connector_rotate"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_test"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_connector_id uuid;
    v_scopes       text[];
    v_started_at   timestamptz := clock_timestamp();
BEGIN
    SELECT c.id INTO v_connector_id
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug
       AND c.is_active = true;

    IF v_connector_id IS NULL THEN
        RETURN jsonb_build_object('ok', false, 'latency_ms', 0, 'scopes', '[]'::jsonb);
    END IF;

    SELECT t.scopes INTO v_scopes
      FROM connectors.connector_tokens t
     WHERE t.connector_id = v_connector_id
       AND t.revoked_at IS NULL
     ORDER BY t.created_at DESC
     LIMIT 1;

    UPDATE connectors.connectors
       SET last_used_at = now()
     WHERE id = v_connector_id;

    RETURN jsonb_build_object(
        'ok',         v_scopes IS NOT NULL,
        'latency_ms', EXTRACT(MILLISECOND FROM clock_timestamp() - v_started_at)::int,
        'scopes',     COALESCE(v_scopes, ARRAY[]::text[])
    );
END
$$;


ALTER FUNCTION "public"."fn_connector_test"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connectors_list"() RETURNS TABLE("slug" "text", "name" "text", "description" "text", "scopes" "text"[], "is_active" boolean, "kind" "text", "created_at" timestamp with time zone, "last_used_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
    SELECT c.slug,
           c.name,
           c.description,
           COALESCE(
               (SELECT t.scopes
                  FROM connectors.connector_tokens t
                 WHERE t.connector_id = c.id
                   AND t.revoked_at IS NULL
                 ORDER BY t.created_at DESC
                 LIMIT 1),
               ARRAY[]::text[]
           ) AS scopes,
           c.is_active,
           c.kind,
           c.created_at,
           c.last_used_at
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
     ORDER BY c.created_at DESC
$$;


ALTER FUNCTION "public"."fn_connectors_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[] DEFAULT '{}'::"uuid"[]) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
  v_thread_id uuid;
  v_lang      text := 'en';
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated or no active lenser profile';
  END IF;

  SELECT coalesce(lp.language, 'en')
  INTO v_lang
  FROM lensers.preferences lp
  WHERE lp.lenser_id = v_lenser_id;

  INSERT INTO content.threads (lenser_id, visibility)
  VALUES (v_lenser_id, p_visibility)
  RETURNING id INTO v_thread_id;

  INSERT INTO content.entity_translations (
    entity_type, entity_id, language_code, is_original, title, content
  ) VALUES (
    'thread', v_thread_id, v_lang, true, p_title, p_content
  );

  IF p_tag_ids IS NOT NULL AND array_length(p_tag_ids, 1) > 0 THEN
    INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
    SELECT 'thread', v_thread_id, unnest(p_tag_ids)
    ON CONFLICT DO NOTHING;
  END IF;

  RETURN v_thread_id;
END;
$$;


ALTER FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  -- Verify the tag is public
  IF NOT EXISTS (
    SELECT 1 FROM content.tags
    WHERE id = p_tag_id AND visibility = 'public'::"content"."tag_visibility_enum"
  ) THEN
    RAISE EXCEPTION 'Tag not found';
  END IF;

  INSERT INTO lensers.tag_follows (lenser_id, tag_id)
  VALUES (v_lenser_id, p_tag_id)
  ON CONFLICT (lenser_id, tag_id) DO NOTHING;

  RETURN jsonb_build_object('following', true);
END;
$$;


ALTER FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") RETURNS TABLE("tag_id" "uuid", "slug" "text", "name" "text", "followed_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content'
    AS $$
  SELECT
    t.id,
    t.slug,
    COALESCE(
      tt_pref.name,   -- preferred language first
      tt_en.name,     -- English fallback
      t.slug          -- final fallback: slug
    ) AS name,
    tf.created_at AS followed_at
  FROM lensers.tag_follows tf
  JOIN content.tags t ON t.id = tf.tag_id
  LEFT JOIN content.tag_translations tt_en
    ON tt_en.tag_id = t.id AND tt_en.language_code = 'en'
  LEFT JOIN content.tag_translations tt_pref
    ON tt_pref.tag_id = t.id
    AND tt_pref.language_code = (
      SELECT lp.language
      FROM lensers.preferences lp
      WHERE lp.lenser_id = p_lenser_id
    )
  WHERE tf.lenser_id = p_lenser_id
    AND t.visibility = 'public'::"content"."tag_visibility_enum"
  ORDER BY tf.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "description" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
WITH current_lenser AS (
  SELECT p.id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.id = p_lenser_id
  LIMIT 1
),
followed_authors AS (
  SELECT DISTINCT r.target_profile_id AS lenser_id
  FROM lensers.relationships r
  WHERE r.source_profile_id = (SELECT id FROM current_lenser)
    AND r.status = 'accepted'
)
SELECT
  v.id,
  v.author_profile,
  v.tags,
  v.reaction_totals,
  v.title,
  v.description,
  v.created_at
FROM public.vw_lenses_public v
JOIN followed_authors fa ON fa.lenser_id = v.lenser_id
ORDER BY v.created_at DESC
LIMIT GREATEST(LEAST(p_limit, 50), 0)
OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Returns public lenses authored by lensers the current viewer follows. SECURITY DEFINER and auth-scoped.';



CREATE OR REPLACE FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "content" "text", "reply_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
WITH current_lenser AS (
  SELECT p.id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.id = p_lenser_id
  LIMIT 1
),
followed_authors AS (
  SELECT DISTINCT r.target_profile_id AS lenser_id
  FROM lensers.relationships r
  WHERE r.source_profile_id = (SELECT id FROM current_lenser)
    AND r.status = 'accepted'
)
SELECT
  v.id,
  log(GREATEST(1,
    2.0 * COALESCE(v.like_count, 0)
    + 3.0 * COALESCE(v.reply_count, 0)
    + 0.5 * COALESCE(v.view_count, 0)
  )) / pow(EXTRACT(epoch FROM (now() - v.created_at)) / 3600.0 + 2, 1.5) AS hot_score,
  et.language_code AS primary_language,
  v.author_profile,
  v.tags,
  v.reaction_totals,
  v.title,
  v.content,
  v.reply_count,
  v.created_at
FROM public.vw_content_threads_public v
JOIN followed_authors fa ON fa.lenser_id = v.lenser_id
LEFT JOIN content.entity_translations et
  ON et.entity_id = v.id
 AND et.entity_type = 'thread'::content.entity_type_enum
 AND et.is_original = true
ORDER BY v.created_at DESC
LIMIT GREATEST(LEAST(p_limit, 50), 0)
OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Returns public threads authored by lensers the current viewer follows. SECURITY DEFINER and auth-scoped.';



CREATE OR REPLACE FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text" DEFAULT 'newest'::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "content"."visibility_enum", "title" "text", "description" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "copy_count" integer, "like_count" integer, "saved_count" integer, "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  WITH matched_ids AS (
    SELECT DISTINCT tm.entity_id AS prompt_id
    FROM content.tag_map tm
    JOIN content.tags tg ON tg.id = tm.tag_id AND tg.slug = p_tag_slug
    WHERE tm.entity_type = 'lens'
    LIMIT 1000
  )
  SELECT
    v.id, v.lenser_id, v.visibility, v.title, v.description,
    v.author_profile, v.reaction_totals, v.copy_count, v.like_count,
    v.saved_count, v.tags, v.created_at
  FROM matched_ids m
  JOIN public.vw_lenses_public v ON v.id = m.prompt_id
  ORDER BY
    CASE WHEN p_sort = 'newest' THEN v.created_at END DESC,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.copy_count END DESC NULLS LAST,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.like_count END DESC NULLS LAST
  LIMIT  LEAST(p_limit, 50)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "personal_score" double precision, "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "description" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
WITH
  current_lenser AS (
    SELECT p.id, COALESCE(lp.language, 'en') AS preferred_language, p.user_id
    FROM lensers.profiles p
    LEFT JOIN lensers.preferences lp ON lp.lenser_id = p.id
    WHERE p.user_id = auth.uid()
    LIMIT 1
  ),
  interest_tags AS (
    SELECT tf.tag_id
    FROM lensers.tag_follows tf
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT xcl.equivalent_tag_id
    FROM lensers.tag_follows tf
    JOIN content.vw_tag_cross_lang xcl ON xcl.source_tag_id = tf.tag_id
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT DISTINCT tm.tag_id
    FROM content.reactions r
    JOIN lenses.lenses   pt2 ON pt2.id = r.entity_id
    JOIN content.tag_map tm  ON tm.entity_id = pt2.id AND tm.entity_type = 'lens'
    WHERE r.entity_type = 'lens'
      AND r.lenser_id   = (SELECT user_id FROM current_lenser)
      AND r.created_at  > now() - interval '30 days'
  ),
  candidates AS (
    SELECT pt.id, pt.created_at, pt.lenser_id
    FROM lenses.lenses pt
    WHERE pt.visibility = 'public'
      AND pt.status     = 'published'
      AND (SELECT id FROM current_lenser) IS NOT NULL
    ORDER BY pt.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS prompt_id,
      COUNT(*) FILTER (WHERE r.reaction = 'copy':: content.reaction_enum) AS copy_count,
      COUNT(*) FILTER (WHERE r.reaction = 'like':: content.reaction_enum) AS like_count,
      COUNT(*) FILTER (WHERE r.reaction = 'saved'::content.reaction_enum) AS saved_count
    FROM content.reactions r
    WHERE r.entity_type = 'lens'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  preliminary_scores AS (
    SELECT
      c.id,
      c.lenser_id,
      et.language_code AS primary_language,
      log(GREATEST(1,
        4.0 * COALESCE(r.copy_count,  0)
      + 2.0 * COALESCE(r.like_count,  0)
      + 1.0 * COALESCE(r.saved_count, 0)
      )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5) AS hot_score,
      (
        0.30 * COALESCE((
          SELECT COUNT(*)::float / GREATEST((SELECT COUNT(*) FROM interest_tags), 1)
          FROM content.tag_map tm
          JOIN interest_tags it ON it.tag_id = tm.tag_id
          WHERE tm.entity_type = 'lens' AND tm.entity_id = c.id
        ), 0.0)
        + 0.25 * CASE
            WHEN et.language_code = (SELECT preferred_language FROM current_lenser) THEN 1.0
            ELSE 0.0
          END
        + 0.20 * LEAST(
            log(GREATEST(1,
              4.0 * COALESCE(r.copy_count,  0)
            + 2.0 * COALESCE(r.like_count,  0)
            + 1.0 * COALESCE(r.saved_count, 0)
            )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5)
            / 2.0, 1.0)
        + 0.10 * CASE WHEN fa.target_profile_id IS NOT NULL THEN 1.0 ELSE 0.0 END
      ) AS preliminary_score
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.prompt_id = c.id
    LEFT JOIN content.entity_translations et
           ON et.entity_id   = c.id
          AND et.entity_type = 'lens'
          AND et.is_original = true
    LEFT JOIN lensers.relationships fa
           ON fa.source_profile_id = (SELECT id FROM current_lenser)
          AND fa.target_profile_id = c.lenser_id
          AND fa.status = 'accepted'
    WHERE c.id NOT IN (
      SELECT target_id FROM content.reports
      WHERE target_type = 'lens'::content.entity_type_enum
      GROUP BY target_id HAVING COUNT(DISTINCT reporter_id) >= 3
    )
    ORDER BY preliminary_score DESC
    LIMIT LEAST(p_limit, 50) * 2
  ),
  candidate_scores AS (
    SELECT
      ps.id,
      ps.primary_language,
      ps.hot_score,
      (
        ps.preliminary_score
        + 0.15 * LEAST(COALESCE(ls.lenser_score, 0.0) / 5.0, 1.0)
      ) AS personal_score
    FROM preliminary_scores ps
    LEFT JOIN lensers.vw_lensers_score ls ON ls.lenser_id = ps.lenser_id
    ORDER BY personal_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT
  v.id, c.personal_score, c.hot_score, c.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, v.description, v.created_at
FROM candidate_scores c
JOIN public.vw_lenses_public v ON v.id = c.id
ORDER BY c.personal_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "personal_score" double precision, "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "content" "text", "reply_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
WITH
  current_lenser AS (
    SELECT p.id, COALESCE(lp.language, 'en') AS preferred_language, p.user_id
    FROM lensers.profiles p
    LEFT JOIN lensers.preferences lp ON lp.lenser_id = p.id
    WHERE p.user_id = auth.uid()
    LIMIT 1
  ),
  interest_tags AS (
    SELECT tf.tag_id
    FROM lensers.tag_follows tf
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT xcl.equivalent_tag_id
    FROM lensers.tag_follows tf
    JOIN content.vw_tag_cross_lang xcl ON xcl.source_tag_id = tf.tag_id
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT DISTINCT tm.tag_id
    FROM content.reactions r
    JOIN content.threads t2 ON t2.id = r.entity_id
    JOIN content.tag_map  tm ON tm.entity_id = t2.id AND tm.entity_type = 'thread'
    WHERE r.entity_type = 'thread'
      AND r.lenser_id   = (SELECT user_id FROM current_lenser)
      AND r.created_at  > now() - interval '30 days'
  ),
  candidates AS (
    SELECT t.id, t.created_at, t.lenser_id, t.reply_count, t.view_count
    FROM content.threads t
    WHERE t.visibility = 'public'
      AND t.status     = 'published'
      AND (SELECT id FROM current_lenser) IS NOT NULL
    ORDER BY t.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS thread_id,
      count(*) FILTER (WHERE r.reaction = 'like'::content.reaction_enum) AS like_count
    FROM content.reactions r
    WHERE r.entity_type = 'thread'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  preliminary_scores AS (
    SELECT
      c.id,
      c.lenser_id,
      c.reply_count,
      et.language_code AS primary_language,
      log(greatest(1,
        2.0 * coalesce(r.like_count, 0)
      + 3.0 * c.reply_count
      + 0.5 * c.view_count
      )) / pow(extract(epoch from (now() - c.created_at)) / 3600.0 + 2, 1.5) AS hot_score,
      (
        0.30 * COALESCE((
          SELECT COUNT(*)::float / GREATEST((SELECT COUNT(*) FROM interest_tags), 1)
          FROM content.tag_map tm
          JOIN interest_tags it ON it.tag_id = tm.tag_id
          WHERE tm.entity_type = 'thread' AND tm.entity_id = c.id
        ), 0.0)
        + 0.25 * CASE
            WHEN et.language_code = (SELECT preferred_language FROM current_lenser) THEN 1.0
            ELSE 0.0
          END
        + 0.20 * LEAST(
            log(greatest(1,
              2.0 * coalesce(r.like_count, 0)
            + 3.0 * c.reply_count
            + 0.5 * c.view_count
            )) / pow(extract(epoch from (now() - c.created_at)) / 3600.0 + 2, 1.5)
            / 2.0, 1.0)
        + 0.10 * CASE WHEN fa.target_profile_id IS NOT NULL THEN 1.0 ELSE 0.0 END
      ) AS preliminary_score
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.thread_id = c.id
    LEFT JOIN content.entity_translations et
           ON et.entity_id   = c.id
          AND et.entity_type = 'thread'
          AND et.is_original = true
    LEFT JOIN lensers.relationships fa
           ON fa.source_profile_id = (SELECT id FROM current_lenser)
          AND fa.target_profile_id = c.lenser_id
          AND fa.status = 'accepted'
    WHERE c.id NOT IN (
      SELECT target_id FROM content.reports
      WHERE target_type = 'thread'::content.entity_type_enum
      GROUP BY target_id HAVING COUNT(DISTINCT reporter_id) >= 3
    )
    ORDER BY preliminary_score DESC
    LIMIT LEAST(p_limit, 50) * 2
  ),
  scored AS (
    SELECT
      ps.id,
      ps.primary_language,
      ps.reply_count,
      ps.hot_score,
      (
        ps.preliminary_score
        + 0.15 * LEAST(COALESCE(ls.lenser_score, 0.0) / 5.0, 1.0)
      ) AS personal_score
    FROM preliminary_scores ps
    LEFT JOIN lensers.vw_lensers_score ls ON ls.lenser_id = ps.lenser_id
    ORDER BY personal_score DESC
    LIMIT  LEAST(p_limit, 50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT v.id, s.personal_score, s.hot_score, s.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, v.content, s.reply_count, v.created_at
FROM scored s
JOIN public.vw_content_threads_public v ON v.id = s.id
ORDER BY s.personal_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "content"."visibility_enum", "title" "text", "description" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "copy_count" integer, "like_count" integer, "saved_count" integer, "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers'
    AS $$
WITH
  candidates AS (
    SELECT pt.id, pt.created_at
    FROM lenses.lenses pt
    WHERE pt.visibility = 'public' AND pt.status = 'published'
    ORDER BY pt.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS prompt_id,
      COUNT(*) FILTER (WHERE r.reaction = 'copy':: content.reaction_enum) AS copy_count,
      COUNT(*) FILTER (WHERE r.reaction = 'like':: content.reaction_enum) AS like_count,
      COUNT(*) FILTER (WHERE r.reaction = 'saved'::content.reaction_enum) AS saved_count
    FROM content.reactions r
    WHERE r.entity_type = 'lens'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  ranked AS (
    SELECT
      c.id,
      log(GREATEST(1,
        4.0 * COALESCE(r.copy_count,  0)
      + 2.0 * COALESCE(r.like_count,  0)
      + 1.0 * COALESCE(r.saved_count, 0)
      )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5) AS hot_score
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.prompt_id = c.id
    ORDER BY hot_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT
  v.id, v.lenser_id, v.visibility, v.title, v.description,
  v.author_profile, v.reaction_totals, v.copy_count, v.like_count,
  v.saved_count, v.tags, v.created_at
FROM ranked rk
JOIN public.vw_lenses_public v ON v.id = rk.id
ORDER BY rk.hot_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text" DEFAULT 'newest'::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "content" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "like_count" integer, "reply_count" integer, "view_count" integer, "visibility" "content"."visibility_enum", "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  WITH matched_ids AS (
    SELECT DISTINCT tm.entity_id AS thread_id
    FROM content.tag_map tm
    JOIN content.tags tg ON tg.id = tm.tag_id AND tg.slug = p_tag_slug
    WHERE tm.entity_type = 'thread'
    LIMIT 1000
  )
  SELECT
    v.id, v.lenser_id, v.title, v.content,
    v.author_profile, v.reaction_totals, v.like_count,
    v.reply_count, v.view_count, v.visibility, v.tags, v.created_at
  FROM matched_ids m
  JOIN public.vw_content_threads_public v ON v.id = m.thread_id
  ORDER BY
    CASE WHEN p_sort = 'newest' THEN v.created_at END DESC,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.like_count END DESC NULLS LAST,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.reply_count END DESC NULLS LAST
  LIMIT  LEAST(p_limit, 50)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "description" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers'
    AS $$
WITH
  candidates AS (
    SELECT pt.id, pt.created_at
    FROM lenses.lenses pt
    WHERE pt.visibility = 'public' AND pt.status = 'published'
    ORDER BY pt.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS prompt_id,
      COUNT(*) FILTER (WHERE r.reaction = 'copy':: content.reaction_enum) AS copy_count,
      COUNT(*) FILTER (WHERE r.reaction = 'like':: content.reaction_enum) AS like_count,
      COUNT(*) FILTER (WHERE r.reaction = 'saved'::content.reaction_enum) AS saved_count
    FROM content.reactions r
    WHERE r.entity_type = 'lens'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  scored AS (
    SELECT
      c.id,
      log(GREATEST(1,
        4.0 * COALESCE(r.copy_count,  0)
      + 2.0 * COALESCE(r.like_count,  0)
      + 1.0 * COALESCE(r.saved_count, 0)
      )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5)
        * CASE WHEN p_lang IS NOT NULL AND et.language_code = p_lang THEN 1.5 ELSE 1.0 END
        AS hot_score,
      et.language_code AS primary_language
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.prompt_id = c.id
    LEFT JOIN content.entity_translations et
           ON et.entity_id   = c.id
          AND et.entity_type = 'lens'
          AND et.is_original = true
    ORDER BY hot_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT v.id, s.hot_score, s.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, v.description, v.created_at
FROM scored s
JOIN public.vw_lenses_public v ON v.id = s.id
ORDER BY s.hot_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "reply_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
WITH
  candidates AS (
    SELECT t.id, t.created_at, t.reply_count, t.view_count
    FROM content.threads t
    WHERE t.visibility = 'public' AND t.status = 'published'
    ORDER BY t.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS thread_id,
      count(*) FILTER (WHERE r.reaction = 'like'::content.reaction_enum) AS like_count
    FROM content.reactions r
    WHERE r.entity_type = 'thread'::content.entity_type_enum
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  scored AS (
    SELECT
      c.id,
      log(greatest(1,
        2.0 * coalesce(ra.like_count, 0)
      + 3.0 * c.reply_count
      + 0.5 * c.view_count
      )) / pow(extract(epoch from (now() - c.created_at)) / 3600.0 + 2, 1.5)
        * CASE WHEN p_lang IS NOT NULL AND ttt.language_code = p_lang THEN 1.5 ELSE 1.0 END
        AS hot_score,
      ttt.language_code AS primary_language,
      c.reply_count
    FROM candidates c
    LEFT JOIN reaction_agg ra ON ra.thread_id = c.id
    LEFT JOIN content.entity_translations ttt ON ttt.entity_id = c.id AND ttt.entity_type = 'thread'::content.entity_type_enum AND ttt.is_original = true
    ORDER BY hot_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT v.id, s.hot_score, s.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, s.reply_count, v.created_at
FROM scored s
JOIN public.vw_content_threads_public v ON v.id = s.id
ORDER BY s.hot_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") RETURNS TABLE("id" "uuid", "target_id" "uuid", "user_id" "uuid", "reaction" "content"."reaction_enum", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_entity    content.entity_type_enum;
  v_type      TEXT := lower(trim(p_target_type));
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN RETURN; END IF;

  IF v_type NOT IN ('thread', 'thread_reply', 'lens', 'workflow', 'battle', 'agent') THEN
    RETURN;
  END IF;

  IF v_type IN ('lens', 'workflow', 'battle', 'agent')
     AND NOT public.fn_artifact_can_view(v_type, p_target_id) THEN
    RETURN;
  END IF;

  v_entity := v_type::content.entity_type_enum;

  RETURN QUERY
    SELECT r.id, r.entity_id AS target_id, r.lenser_id AS user_id, r.reaction, r.created_at
    FROM content.reactions r
    WHERE r.entity_type = v_entity
      AND r.entity_id = p_target_id
      AND r.lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_added     BOOLEAN;
  v_counts    JSONB;
  v_entity    content.entity_type_enum;
  v_type      TEXT := lower(trim(p_target_type));
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = '42501';
  END IF;

  IF v_type NOT IN ('thread', 'thread_reply', 'lens', 'workflow', 'battle', 'agent') THEN
    RAISE EXCEPTION 'Invalid target_type: %', p_target_type USING ERRCODE = '22023';
  END IF;

  IF v_type IN ('lens', 'workflow', 'battle', 'agent')
     AND NOT public.fn_artifact_can_view(v_type, p_target_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_entity := v_type::content.entity_type_enum;

  IF p_reaction = 'copy'::content.reaction_enum THEN
    INSERT INTO content.reactions (entity_type, entity_id, lenser_id, reaction)
    VALUES (v_entity, p_target_id, v_lenser_id, p_reaction);
    v_added := TRUE;
  ELSE
    IF EXISTS (
      SELECT 1
      FROM content.reactions
      WHERE entity_type = v_entity
        AND entity_id = p_target_id
        AND lenser_id = v_lenser_id
        AND reaction = p_reaction
    ) THEN
      DELETE FROM content.reactions
      WHERE entity_type = v_entity
        AND entity_id = p_target_id
        AND lenser_id = v_lenser_id
        AND reaction = p_reaction;
      v_added := FALSE;
    ELSE
      INSERT INTO content.reactions (entity_type, entity_id, lenser_id, reaction)
      VALUES (v_entity, p_target_id, v_lenser_id, p_reaction);
      v_added := TRUE;
    END IF;
  END IF;

  SELECT COALESCE(jsonb_object_agg(reaction, cnt), '{}'::jsonb)
  INTO v_counts
  FROM (
    SELECT reaction, count(*)::int AS cnt
    FROM content.reactions
    WHERE entity_type = v_entity
      AND entity_id = p_target_id
    GROUP BY reaction
  ) s;

  RETURN jsonb_build_object('added', v_added, 'counts', v_counts);
END;
$$;


ALTER FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text" DEFAULT 'other'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content', 'auth'
    AS $$
DECLARE
  v_lenser_id  uuid;
  v_target_type "content"."entity_type_enum";
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  -- Validate target_type against the enum
  BEGIN
    v_target_type := p_target_type::"content"."entity_type_enum";
  EXCEPTION WHEN invalid_text_representation THEN
    RAISE EXCEPTION 'Invalid target_type: %', p_target_type;
  END;

  -- Validate and cast reason to enum
  IF p_reason NOT IN ('spam', 'harassment', 'misinformation', 'off_topic', 'other') THEN
    RAISE EXCEPTION 'Invalid reason: %', p_reason;
  END IF;

  -- Cooldown: reject if the same reporter already reported this target within 24 hours
  IF EXISTS (
    SELECT 1 FROM content.reports
    WHERE reporter_id = v_lenser_id
      AND target_type = v_target_type
      AND target_id   = p_target_id
      AND created_at  > NOW() - INTERVAL '24 hours'
  ) THEN
    RAISE EXCEPTION 'rate_limit_exceeded'
      USING DETAIL = 'You have already reported this content recently.',
            ERRCODE = 'P0429';
  END IF;

  INSERT INTO content.reports (reporter_id, target_type, target_id, reason)
  VALUES (v_lenser_id, v_target_type, p_target_id, p_reason::"content"."report_reason_enum")
  ON CONFLICT (reporter_id, target_type, target_id)
  DO UPDATE SET reason = EXCLUDED.reason;

  RETURN jsonb_build_object('reported', true);
END;
$$;


ALTER FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'public'
    AS $$
DECLARE
  v_id uuid;
BEGIN
  IF p_name IS NULL OR btrim(p_name) = '' THEN
    RAISE EXCEPTION 'Tag name is required';
  END IF;

  IF p_slug IS NULL OR btrim(p_slug) = '' THEN
    RAISE EXCEPTION 'Tag slug is required';
  END IF;

  INSERT INTO content.tags (slug, visibility)
  VALUES (btrim(p_slug), 'public')
  RETURNING id INTO v_id;

  INSERT INTO content.tag_translations (tag_id, language_code, name)
  VALUES (v_id, 'en', btrim(p_name));

  RETURN v_id;
EXCEPTION
  WHEN unique_violation THEN
    SELECT id INTO v_id
    FROM content.tags
    WHERE slug = p_slug;

    IF v_id IS NOT NULL THEN
      RETURN v_id;
    END IF;

    RAISE;
END;
$$;


ALTER FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") RETURNS TABLE("id" "uuid", "name" "text", "slug" "text", "visibility" "content"."tag_visibility_enum", "created_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT t.id, t.name, t.slug, t.visibility, t.created_at
  FROM content.tags t
  WHERE t.slug = p_slug
    AND t.visibility = 'public';
$$;


ALTER FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  DELETE FROM lensers.tag_follows
  WHERE lenser_id = v_lenser_id AND tag_id = p_tag_id;

  RETURN jsonb_build_object('following', false);
END;
$$;


ALTER FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_core_languages_list"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'core'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  SELECT jsonb_agg(
    jsonb_build_object(
      'code',        l.code,
      'name',        l.name,
      'native_name', l.native_name,
      'direction',   l.direction
    )
    ORDER BY l.name
  )
  INTO v_result
  FROM core.languages l
  WHERE l.is_active = true;

  RETURN COALESCE(v_result, '[]'::jsonb);
END;
$$;


ALTER FUNCTION "public"."fn_core_languages_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) RETURNS TABLE("reservation_id" "uuid", "status" "text", "committed_credits" numeric, "committed_usd" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'agents', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row     billing.cost_reservations%ROWTYPE;
  v_credits numeric;
BEGIN
  IF p_reservation_id IS NULL OR p_actual_credits IS NULL OR p_actual_credits < 0
     OR p_actual_usd IS NULL OR p_actual_usd < 0 THEN
    RAISE EXCEPTION 'fn_cost_commit: bad input'
      USING ERRCODE = '22023';
  END IF;

  SELECT cr.* INTO v_row FROM billing.cost_reservations cr
  WHERE cr.id = p_reservation_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_commit: reservation % not found', p_reservation_id
      USING ERRCODE = '23503';
  END IF;
  IF v_row.status = 'committed' THEN
    -- Idempotent: return current state.
    RETURN QUERY
    SELECT v_row.id, v_row.status, v_row.committed_credits, v_row.committed_usd;
    RETURN;
  END IF;
  IF v_row.status <> 'held' THEN
    RAISE EXCEPTION 'fn_cost_commit: reservation % cannot commit from status %',
      p_reservation_id, v_row.status
      USING ERRCODE = 'P0001';
  END IF;

  v_credits := CEIL(p_actual_credits);

  -- Reverse the original hold and post the actual commit (double-entry).
  INSERT INTO billing.ledger_entries (
    reservation_id, ai_lenser_id, org_id, direction, amount_credits, amount_usd
  )
  VALUES
    (v_row.id, v_row.ai_lenser_id, v_row.org_id, 'release',
       -v_row.reserved_credits, -v_row.reserved_usd),
    (v_row.id, v_row.ai_lenser_id, v_row.org_id, 'debit_commit',
       p_actual_credits, p_actual_usd);

  UPDATE billing.cost_reservations cr
     SET status            = 'committed',
         committed_credits = p_actual_credits,
         committed_usd     = p_actual_usd,
         committed_at      = now()
   WHERE cr.id = v_row.id;

  -- Settle into the legacy daily snapshot (kept as a materialized projection
  -- of the ledger so existing budget readers keep working).
  INSERT INTO agents.quota_snapshots AS qs (ai_lenser_id, period_date, credits_spent)
  VALUES (v_row.ai_lenser_id, CURRENT_DATE, v_credits)
  ON CONFLICT (ai_lenser_id, period_date) DO UPDATE
    SET credits_spent = qs.credits_spent + EXCLUDED.credits_spent,
        updated_at    = now();

  PERFORM automation.fn_emit_event(
    'billing.budget_committed',
    'billing', 'cost_reservations', v_row.id,
    jsonb_build_object(
      'ai_lenser_id',      v_row.ai_lenser_id,
      'committed_credits', p_actual_credits,
      'committed_usd',     p_actual_usd
    ),
    NULL
  );

  RETURN QUERY
  SELECT v_row.id, 'committed'::text, p_actual_credits, p_actual_usd;
END;
$$;


ALTER FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) IS 'CG1: held -> committed. Posts reversal of debit_hold + new debit_commit. Settles agents.quota_snapshots so legacy budget readers see the spend. Idempotent on already-committed reservations.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer DEFAULT 200) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_id    uuid;
  v_count integer := 0;
BEGIN
  -- Claim with SKIP LOCKED so multiple cron workers cannot collide.
  FOR v_id IN
    SELECT id
    FROM   billing.cost_reservations
    WHERE  status = 'held' AND held_until <= now()
    ORDER  BY held_until
    LIMIT  GREATEST(p_max_batch, 1)
    FOR    UPDATE SKIP LOCKED
  LOOP
    BEGIN
      PERFORM public.fn_cost_release(v_id, 'worker_timeout');
      v_count := v_count + 1;
    EXCEPTION WHEN OTHERS THEN
      -- Defensive: a failed release for one reservation should not abort the
      -- whole batch. Mark expired without a ledger reversal as a last resort.
      UPDATE billing.cost_reservations
         SET status = 'expired', released_at = now(), reason_released = 'expire_fallback:' || SQLERRM
       WHERE id = v_id AND status = 'held';
    END;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) IS 'CG1: cron-driven sweeper that releases reservations whose held_until passed. Uses FOR UPDATE SKIP LOCKED for safe concurrent workers.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) RETURNS TABLE("status" "text", "over_limit" boolean, "reserved_credits" numeric, "running_credits" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row       billing.cost_reservations%ROWTYPE;
  v_headroom  smallint;
  v_over      boolean := false;
BEGIN
  IF p_reservation_id IS NULL OR p_running_credits IS NULL OR p_running_credits < 0 THEN
    RAISE EXCEPTION 'fn_cost_meter_tick: bad input'
      USING ERRCODE = '22023';
  END IF;

  SELECT cr.* INTO v_row FROM billing.cost_reservations cr
  WHERE cr.id = p_reservation_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_meter_tick: reservation % not found', p_reservation_id
      USING ERRCODE = '23503';
  END IF;
  IF v_row.status <> 'held' THEN
    RAISE EXCEPTION 'fn_cost_meter_tick: reservation % not held (status=%)',
      p_reservation_id, v_row.status
      USING ERRCODE = 'P0001';
  END IF;

  SELECT rs.meter_headroom_pct INTO v_headroom
  FROM billing.runtime_settings rs WHERE rs.id = 1;
  v_over := p_running_credits > v_row.reserved_credits * (1 + v_headroom::numeric / 100);

  UPDATE billing.cost_reservations cr
     SET running_credits = GREATEST(cr.running_credits, p_running_credits)
   WHERE cr.id = p_reservation_id;

  RETURN QUERY
  SELECT v_row.status, v_over, v_row.reserved_credits, GREATEST(v_row.running_credits, p_running_credits);
END;
$$;


ALTER FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) IS 'CG1: monotonically updates running_credits and returns over_limit=true when running credits exceed reserved * (1 + headroom_pct). CG5 wires workers.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer DEFAULT 0, "p_est_max_output_tokens" integer DEFAULT 0, "p_units" numeric DEFAULT 0) RETURNS TABLE("pricing_snapshot_id" "uuid", "unit_type" "ai"."unit_type_enum", "estimated_usd" numeric, "estimated_credits" numeric, "credit_rate_usd" numeric, "taken_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'ai', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_pricing    ai.model_pricing%ROWTYPE;
  v_snap_id    uuid;
  v_usd        numeric(20,10);
  v_rate       numeric(20,10) := 0.001;       -- TODO(CG8): per-org override
  v_credits    numeric(20,6);
BEGIN
  IF p_model_id IS NULL THEN
    RAISE EXCEPTION 'fn_cost_quote: p_model_id is required'
      USING ERRCODE = '22023';
  END IF;
  IF p_est_input_tokens < 0 OR p_est_max_output_tokens < 0 OR p_units < 0 THEN
    RAISE EXCEPTION 'fn_cost_quote: negative usage not allowed'
      USING ERRCODE = '22023';
  END IF;

  SELECT *
  INTO   v_pricing
  FROM   ai.model_pricing
  WHERE  model_id = p_model_id
    AND  effective_from <= now()
    AND  (effective_to IS NULL OR effective_to > now())
  ORDER  BY effective_from DESC
  LIMIT  1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'E_PRICING_UNAVAILABLE: no active price row for model %', p_model_id
      USING ERRCODE = 'P0001';
  END IF;

  IF v_pricing.unit_type = 'tokens' THEN
    v_usd :=
        (p_est_input_tokens::numeric    / 1000.0) * v_pricing.input_cost_per_1k_tokens
      + (p_est_max_output_tokens::numeric / 1000.0) * v_pricing.output_cost_per_1k_tokens;
  ELSE
    v_usd := p_units * COALESCE(v_pricing.cost_per_unit, 0);
  END IF;

  -- Reuse a recent snapshot for the same source pricing row when present
  -- (under 5 min old) to avoid table bloat on hot paths. Alias the table so
  -- the OUT-column `taken_at` does not collide with the table column.
  SELECT ps.id
  INTO   v_snap_id
  FROM   billing.pricing_snapshots ps
  WHERE  ps.source_pricing_id = v_pricing.id
    AND  ps.taken_at > now() - interval '5 minutes'
  ORDER  BY ps.taken_at DESC
  LIMIT  1;

  IF v_snap_id IS NULL THEN
    INSERT INTO billing.pricing_snapshots (
      model_id, source_pricing_id, unit_type,
      input_cpm_usd, output_cpm_usd, unit_cost_usd, credit_rate_usd
    )
    VALUES (
      p_model_id, v_pricing.id, v_pricing.unit_type,
      v_pricing.input_cost_per_1k_tokens,
      v_pricing.output_cost_per_1k_tokens,
      v_pricing.cost_per_unit,
      v_rate
    )
    RETURNING id INTO v_snap_id;
  END IF;

  v_credits := CEIL(v_usd / v_rate);

  RETURN QUERY
  SELECT v_snap_id, v_pricing.unit_type, v_usd::numeric, v_credits, v_rate::numeric, now();
END;
$$;


ALTER FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) IS 'CG1: returns an estimated dollar/credit cost AND pins a pricing_snapshot row that fn_cost_reserve will reference. Safe for authenticated callers (read-only pricing; no side effects beyond the immutable snapshot insert).';



CREATE OR REPLACE FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") RETURNS TABLE("reservation_id" "uuid", "status" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row billing.cost_reservations%ROWTYPE;
BEGIN
  IF p_reservation_id IS NULL THEN
    RAISE EXCEPTION 'fn_cost_release: reservation_id required'
      USING ERRCODE = '22023';
  END IF;

  SELECT cr.* INTO v_row FROM billing.cost_reservations cr
  WHERE cr.id = p_reservation_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_release: reservation % not found', p_reservation_id
      USING ERRCODE = '23503';
  END IF;
  IF v_row.status IN ('released','expired') THEN
    RETURN QUERY SELECT v_row.id, v_row.status;
    RETURN;
  END IF;
  IF v_row.status <> 'held' THEN
    RAISE EXCEPTION 'fn_cost_release: reservation % cannot release from status %',
      p_reservation_id, v_row.status
      USING ERRCODE = 'P0001';
  END IF;

  INSERT INTO billing.ledger_entries (
    reservation_id, ai_lenser_id, org_id, direction, amount_credits, amount_usd
  )
  VALUES (
    v_row.id, v_row.ai_lenser_id, v_row.org_id, 'release',
    -v_row.reserved_credits, -v_row.reserved_usd
  );

  UPDATE billing.cost_reservations cr
     SET status           = 'released',
         reason_released  = p_reason,
         released_at      = now()
   WHERE cr.id = v_row.id;

  PERFORM automation.fn_emit_event(
    'billing.budget_released',
    'billing', 'cost_reservations', v_row.id,
    jsonb_build_object(
      'ai_lenser_id', v_row.ai_lenser_id,
      'reason',       COALESCE(p_reason, ''),
      'amount_credits', v_row.reserved_credits
    ),
    NULL
  );

  RETURN QUERY SELECT v_row.id, 'released'::text;
END;
$$;


ALTER FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") IS 'CG1: held -> released. Posts a release ledger entry reversing the hold. Idempotent on already-released/expired reservations.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb" DEFAULT '{}'::"jsonb", "p_actor_id" "uuid" DEFAULT NULL::"uuid", "p_org_id" "uuid" DEFAULT NULL::"uuid", "p_ttl" interval DEFAULT NULL::interval) RETURNS TABLE("reservation_id" "uuid", "status" "text", "held_until" timestamp with time zone, "shadow_mode" boolean)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'agents', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_settings       billing.runtime_settings%ROWTYPE;
  v_snap           billing.pricing_snapshots%ROWTYPE;
  v_existing       billing.cost_reservations%ROWTYPE;
  v_lenser         agents.ai_lensers%ROWTYPE;
  v_policy_limit   numeric;
  v_already_held   numeric;
  v_already_today  numeric;
  v_kill_switch    boolean;
  v_paused         boolean;
  v_budget_enforce boolean := false;
  v_id             uuid;
  v_ttl            interval;
BEGIN
  IF p_ai_lenser_id IS NULL OR p_pricing_snapshot_id IS NULL
     OR p_reserved_credits IS NULL OR p_provider_key IS NULL
     OR p_idempotency_key IS NULL OR LENGTH(p_idempotency_key) = 0 THEN
    RAISE EXCEPTION 'fn_cost_reserve: missing required parameter'
      USING ERRCODE = '22023';
  END IF;
  IF p_reserved_credits <= 0 THEN
    RAISE EXCEPTION 'fn_cost_reserve: reserved_credits must be > 0'
      USING ERRCODE = '22023';
  END IF;
  IF p_reserved_usd IS NULL OR p_reserved_usd < 0 THEN
    RAISE EXCEPTION 'fn_cost_reserve: reserved_usd must be >= 0'
      USING ERRCODE = '22023';
  END IF;

  SELECT * INTO v_settings FROM billing.runtime_settings WHERE id = 1;
  v_ttl := COALESCE(p_ttl, v_settings.reservation_default_ttl);

  -- Idempotency short-circuit — replay returns the original row unchanged.
  SELECT cr.* INTO v_existing
  FROM   billing.cost_reservations cr
  WHERE  cr.ai_lenser_id = p_ai_lenser_id
    AND  cr.idempotency_key = p_idempotency_key;
  IF FOUND THEN
    RETURN QUERY
    SELECT v_existing.id, v_existing.status, v_existing.held_until, v_existing.shadow_mode;
    RETURN;
  END IF;

  SELECT ps.* INTO v_snap FROM billing.pricing_snapshots ps WHERE ps.id = p_pricing_snapshot_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_reserve: pricing_snapshot % not found', p_pricing_snapshot_id
      USING ERRCODE = '23503';
  END IF;

  SELECT al.* INTO v_lenser FROM agents.ai_lensers al WHERE al.id = p_ai_lenser_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_reserve: ai_lenser % not found', p_ai_lenser_id
      USING ERRCODE = '23503';
  END IF;
  IF v_lenser.is_active = false OR v_lenser.suspended_at IS NOT NULL THEN
    RAISE EXCEPTION 'E_AGENT_PAUSED: ai_lenser % is not active', p_ai_lenser_id
      USING ERRCODE = 'P0001';
  END IF;

  -- Workspace kill switch + budget enforcement flag (both on workspace_settings).
  SELECT COALESCE(ws.global_kill_switch, false),
         COALESCE(ws.agent_paused,       false),
         COALESCE(ws.budget_enforce,     false)
  INTO   v_kill_switch, v_paused, v_budget_enforce
  FROM   agents.workspace_settings ws
  WHERE  ws.ai_lenser_id = p_ai_lenser_id;
  IF v_kill_switch OR v_paused THEN
    RAISE EXCEPTION 'E_KILL_SWITCH: workspace or agent is paused'
      USING ERRCODE = 'P0001';
  END IF;

  -- Legacy daily-credit limit (agents.policies.spending_limit_credits).
  -- CG2 supersedes by consulting billing.budget_policies.
  IF v_budget_enforce THEN
    SELECT pol.spending_limit_credits
    INTO   v_policy_limit
    FROM   agents.policies pol
    WHERE  pol.ai_lenser_id = p_ai_lenser_id;
  END IF;

  IF v_policy_limit IS NOT NULL AND v_settings.enforce THEN
    SELECT COALESCE(SUM(cr.reserved_credits), 0)
    INTO   v_already_held
    FROM   billing.cost_reservations cr
    WHERE  cr.ai_lenser_id = p_ai_lenser_id
      AND  cr.status = 'held'
      AND  cr.held_until > now();

    SELECT COALESCE(qs.credits_spent, 0)
    INTO   v_already_today
    FROM   agents.quota_snapshots qs
    WHERE  qs.ai_lenser_id = p_ai_lenser_id
      AND  qs.period_date  = CURRENT_DATE;

    IF (COALESCE(v_already_held, 0) + COALESCE(v_already_today, 0) + p_reserved_credits) > v_policy_limit THEN
      RAISE EXCEPTION
        'E_BUDGET_EXCEEDED: ai_lenser % would breach daily limit % (held=% today=% requested=%)',
        p_ai_lenser_id, v_policy_limit,
        v_already_held, v_already_today, p_reserved_credits
        USING ERRCODE = 'P0001';
    END IF;
  END IF;

  -- Persist reservation + paired debit_hold ledger entry atomically.
  INSERT INTO billing.cost_reservations (
    ai_lenser_id, actor_id, org_id,
    model_id, provider_key, pricing_snapshot_id,
    reserved_credits, reserved_usd,
    status, context, idempotency_key, shadow_mode,
    held_until
  )
  VALUES (
    p_ai_lenser_id, p_actor_id, p_org_id,
    v_snap.model_id, p_provider_key, p_pricing_snapshot_id,
    p_reserved_credits, p_reserved_usd,
    'held', COALESCE(p_context, '{}'::jsonb), p_idempotency_key,
    NOT v_settings.enforce,
    now() + v_ttl
  )
  RETURNING id INTO v_id;

  INSERT INTO billing.ledger_entries (
    reservation_id, ai_lenser_id, org_id, direction,
    amount_credits, amount_usd
  )
  VALUES (
    v_id, p_ai_lenser_id, p_org_id, 'debit_hold',
    p_reserved_credits, p_reserved_usd
  );

  PERFORM automation.fn_emit_event(
    'billing.budget_reserved',
    'billing', 'cost_reservations', v_id,
    jsonb_build_object(
      'ai_lenser_id',     p_ai_lenser_id,
      'reserved_credits', p_reserved_credits,
      'reserved_usd',     p_reserved_usd,
      'provider_key',     p_provider_key,
      'shadow_mode',      NOT v_settings.enforce
    ),
    NULL
  );

  RETURN QUERY
  SELECT v_id, 'held'::text, now() + v_ttl, NOT v_settings.enforce;
END;
$$;


ALTER FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) IS 'CG1: creates a held reservation + debit_hold ledger entry. Service_role only. Idempotent on (ai_lenser_id, idempotency_key). Fails closed on budget when billing.runtime_settings.enforce = true.';



CREATE OR REPLACE FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text" DEFAULT NULL::"text", "p_initial_members" "jsonb" DEFAULT '[]'::"jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
  v_team_id   uuid;
  v_team      jsonb;
  v_member    jsonb;
  v_members   jsonb := COALESCE(p_initial_members, '[]'::jsonb);
BEGIN
  IF jsonb_typeof(v_members) <> 'array' THEN
    RAISE EXCEPTION 'p_initial_members must be a JSON array' USING ERRCODE = '22023';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = v_lenser_id
      AND o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'create_team_forbidden' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.teams (ai_lenser_id, name, description, status, is_active)
  VALUES (p_ai_lenser_id, p_name, p_description, 'active', true)
  RETURNING id, to_jsonb(agents.teams.*) INTO v_team_id, v_team;

  FOR v_member IN SELECT * FROM jsonb_array_elements(v_members) LOOP
    INSERT INTO agents.team_members (
      team_id, agent_id, role, responsibility, lane, sort_order, is_active
    ) VALUES (
      v_team_id,
      (v_member->>'agent_id')::uuid,
      v_member->>'role',
      v_member->>'responsibility',
      v_member->>'lane',
      COALESCE((v_member->>'sort_order')::integer, 0),
      COALESCE((v_member->>'is_active')::boolean, true)
    );
  END LOOP;

  RETURN v_team;
END;
$$;


ALTER FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'auth'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  v_result := agents.fn_create_ai_lenser(
    p_owner_lenser_id := p_owner_lenser_id,
    p_handle          := p_handle,
    p_display_name    := p_display_name,
    p_ai_model_id     := p_ai_model_id
  );
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") IS 'Public wrapper for agents.fn_create_ai_lenser. Auth enforced by inner function. Fixes PGRST202 404 in agentsRepository.createAgent.';



CREATE OR REPLACE FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_catalog'
    AS $$
BEGIN
  RETURN public.fn_insert_notification(
    p_lenser_id,
    'automation_action',
    p_title,
    p_body,
    NULL,
    COALESCE(p_payload, '{}'::jsonb)
  );
END;
$$;


ALTER FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") IS 'Phase U2: thin wrapper around public.fn_insert_notification used by automation.fn_dispatch_action when action_kind=notify. Type is fixed to "automation_action" so users can filter automation-driven entries from human-driven ones in their feed.';



CREATE OR REPLACE FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text" DEFAULT 'open'::"text", "p_handicap_config" "jsonb" DEFAULT '{}'::"jsonb", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_lens_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_slug      text;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  v_slug := lower(regexp_replace(p_title, '[^a-z0-9\s-]', '', 'gi'));
  v_slug := regexp_replace(v_slug, '\s+', '-', 'g');
  v_slug := regexp_replace(v_slug, '-+', '-', 'g');
  v_slug := left(v_slug, 80) || '-' || substr(md5(random()::text), 1, 6);

  RETURN QUERY
  INSERT INTO battles.battles (
    title, task_prompt, battle_type, voter_eligibility,
    handicap_config, creator_lenser_id, slug, status,
    workflow_id, lens_id
  ) VALUES (
    p_title, p_task_prompt, p_battle_type, p_voter_eligibility,
    COALESCE(p_handicap_config, '{}'::jsonb), v_lenser_id, v_slug, 'draft',
    p_workflow_id, p_lens_id
  )
  RETURNING
    id, slug, title, task_prompt, status, total_vote_count, published_at,
    voting_opens_at, voting_closes_at, battle_type, voter_eligibility, handicap_config,
    creator_lenser_id, forum_thread_id, workflow_id, lens_id,
    execution_starts_at, auto_publish, voting_duration_hours, vote_velocity, og_image_url,
    winner_contender_id, parent_battle_id, deleted_at;
END;
$$;


ALTER FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text" DEFAULT 'lifecycle'::"text") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'extensions', 'public'
    AS $$
DECLARE
  v_actor UUID := lensers.get_auth_lenser_id();
  v_rules_json JSONB;
  v_hash TEXT;
BEGIN
  SELECT jsonb_build_object(
    'snapshot_reason', p_reason,
    'title', b.title,
    'slug', b.slug,
    'task_prompt', b.task_prompt,
    'rubric_id', b.rubric_id,
    'status', b.status,
    'battle_type', b.battle_type,
    'contender_structure', b.contender_structure,
    'judging_mode', b.judging_mode,
    'task_source', b.task_source,
    'challenge_type', b.challenge_type,
    'generated_challenge_id', b.generated_challenge_id,
    'voter_eligibility', b.voter_eligibility,
    'max_contenders', b.max_contenders,
    'handicap_config', b.handicap_config,
    'workflow_id', b.workflow_id,
    'lens_id', b.lens_id,
    'content_type', b.content_type,
    'execution_starts_at', b.execution_starts_at,
    'auto_publish', b.auto_publish,
    'voting_duration_hours', b.voting_duration_hours,
    'ai_judge_enabled', b.ai_judge_enabled,
    'ai_judge_model_key', b.ai_judge_model_key,
    'ai_judge_prompt_hash', CASE
      WHEN b.ai_judge_prompt IS NULL THEN NULL
      ELSE encode(extensions.digest(b.ai_judge_prompt, 'sha256'), 'hex')
    END,
    'published_at', b.published_at,
    'captured_at', now()
  )
  INTO v_rules_json
  FROM battles.battles b
  WHERE b.id = p_battle_id;

  IF v_rules_json IS NULL THEN
    RAISE EXCEPTION 'battle_not_found: %', p_battle_id USING ERRCODE = 'P0002';
  END IF;

  v_hash := encode(extensions.digest(v_rules_json::text, 'sha256'), 'hex');

  INSERT INTO battles.rule_snapshots (battle_id, snapshot_hash, rules_json, created_by)
  VALUES (p_battle_id, v_hash, v_rules_json, v_actor);

  PERFORM public.fn_log_artifact_lifecycle_event(
    'battle',
    p_battle_id,
    'config_snapshot_create',
    v_actor,
    jsonb_build_object('snapshot_hash', v_hash, 'reason', p_reason)
  );

  RETURN v_hash;
END;
$$;


ALTER FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer DEFAULT 3) RETURNS "battles"."series"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid       UUID := auth.uid();
  v_template  battles.templates%ROWTYPE;
  v_series    battles.series%ROWTYPE;
  v_battle_id UUID;
  v_slug      TEXT;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_round_count IS NULL OR p_round_count < 1 OR p_round_count > 16 THEN
    RAISE EXCEPTION 'invalid_round_count' USING ERRCODE = '22023';
  END IF;

  SELECT * INTO v_template
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL
     AND (is_public = true OR creator_lenser_id = v_uid);
  IF v_template IS NULL THEN
    RAISE EXCEPTION 'template_not_accessible' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.series (title, template_id, creator_lenser_id, round_count)
  VALUES (p_title, p_template_id, v_uid, p_round_count)
  RETURNING * INTO v_series;

  v_slug := lower(regexp_replace(p_title, '[^a-zA-Z0-9]+', '-', 'g'))
            || '-r1-' || substr(v_series.id::text, 1, 8);

  v_battle_id := public.fn_battles_create_from_template(
    p_template_id, p_title || ' — Round 1', v_slug
  );

  INSERT INTO battles.series_rounds (series_id, round_number, battle_id)
  VALUES (v_series.id, 1, v_battle_id);

  RETURN v_series;
END $$;


ALTER FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_next_version integer;
  v_result       jsonb;
BEGIN
  -- Mark existing rubrics as not current
  UPDATE agents.evaluation_rubrics
  SET is_current = false
  WHERE evaluation_id = p_evaluation_id;

  -- Get next version
  SELECT COALESCE(MAX(version), 0) + 1 INTO v_next_version
  FROM agents.evaluation_rubrics
  WHERE evaluation_id = p_evaluation_id;

  INSERT INTO agents.evaluation_rubrics (evaluation_id, version, criteria, is_current)
  VALUES (p_evaluation_id, v_next_version, p_criteria, true)
  RETURNING to_jsonb(agents.evaluation_rubrics.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text" DEFAULT NULL::"text", "p_scoring_rules" "jsonb" DEFAULT '{}'::"jsonb", "p_dataset_uri" "text" DEFAULT NULL::"text", "p_cases" "jsonb" DEFAULT '[]'::"jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_eval_id uuid;
  v_case    jsonb;
  v_result  jsonb;
BEGIN
  INSERT INTO agents.evaluations
    (owner_lenser_id, ai_lenser_id, target_type, target_id, name, description, scoring_rules, dataset_uri)
  VALUES
    (p_owner_lenser_id, p_ai_lenser_id, p_target_type, p_target_id, p_name, p_description,
     COALESCE(p_scoring_rules, '{}'), p_dataset_uri)
  RETURNING id, to_jsonb(agents.evaluations.*) INTO v_eval_id, v_result;

  IF jsonb_array_length(COALESCE(p_cases, '[]'::jsonb)) > 0 THEN
    FOR v_case IN SELECT * FROM jsonb_array_elements(p_cases) LOOP
      INSERT INTO agents.evaluation_cases
        (evaluation_id, input, expected, weight, tags)
      VALUES (
        v_eval_id,
        v_case->'input',
        v_case->'expected',
        COALESCE((v_case->>'weight')::integer, 1),
        COALESCE(ARRAY(SELECT jsonb_array_elements_text(v_case->'tags')), '{}')
      );
    END LOOP;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_language_code" "text" DEFAULT 'en'::"text", "p_params" "jsonb" DEFAULT '[]'::"jsonb", "p_tag_ids" "uuid"[] DEFAULT '{}'::"uuid"[], "p_parent_lens_id" "uuid" DEFAULT NULL::"uuid", "p_forked_from_execution_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_id uuid;
BEGIN
  v_id := lenses.fn_create_lens(
    p_visibility               := p_visibility::content.visibility_enum,
    p_template_body            := p_template_body,
    p_title                    := p_title,
    p_description              := p_description,
    p_language_code            := p_language_code,
    p_params                   := p_params,
    p_tag_ids                  := p_tag_ids,
    p_parent_lens_id           := p_parent_lens_id,
    p_forked_from_execution_id := p_forked_from_execution_id
  );
  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") IS 'Public wrapper for lenses.fn_create_lens. Auth enforced by inner function. Fixes PGRST202 404 in lensesRepository.createLens.';



CREATE OR REPLACE FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text" DEFAULT NULL::"text", "p_external_url" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "owner_lenser_id" "uuid", "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "content_text" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'media', 'lensers'
    AS $$
  INSERT INTO media.objects
    (workspace_id, owner_lenser_id, media_type, mime_type, name,
     content_text, external_url, lifecycle_state)
  VALUES (
    p_workspace_id, lensers.get_auth_lenser_id(), p_media_type, p_mime_type, p_name,
    p_content_text, p_external_url,
    CASE WHEN p_content_text IS NOT NULL OR p_external_url IS NOT NULL
         THEN 'active' ELSE 'pending' END
  )
  RETURNING
    id, workspace_id, owner_lenser_id, bucket, object_key, external_url,
    mime_type, media_type, name, content_text, byte_size, visibility,
    lifecycle_state, metadata, created_at, updated_at;
$$;


ALTER FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") IS 'Security wrapper: insert a media.objects row for the current user. lifecycle_state is auto-set to "active" when content or URL is provided, else "pending".';



CREATE OR REPLACE FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run               agents.team_runs;
  v_total_steps       integer;
  v_total_tools       integer;
  v_total_cost        numeric;
  v_total_mem_writes  integer;
  v_eval_score        numeric;
  v_outcome           text;
  v_title             text;
  v_metrics           jsonb;
  v_step_statuses     jsonb;
  v_tool_statuses     jsonb;
  v_duration_secs     numeric;
  v_report_id         uuid;
BEGIN
  SELECT * INTO v_run
    FROM agents.team_runs
   WHERE id = p_team_run_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'team run not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_run.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF v_run.status NOT IN ('completed','failed','cancelled') THEN
    RAISE EXCEPTION 'run must be terminal before creating a report' USING ERRCODE = '22023';
  END IF;

  IF EXISTS (
    SELECT 1 FROM agents.run_reports WHERE team_run_id = p_team_run_id
  ) THEN
    RAISE EXCEPTION 'report already exists for this run' USING ERRCODE = '23505';
  END IF;

  -- Aggregate step count
  SELECT COUNT(*)
    INTO v_total_steps
    FROM agents.agent_run_steps
   WHERE team_run_id = p_team_run_id;

  -- Aggregate tool count and cost
  SELECT
    COUNT(*),
    COALESCE(SUM(cost_estimate), 0)
  INTO v_total_tools, v_total_cost
    FROM agents.tool_invocations
   WHERE team_run_id = p_team_run_id;

  -- Aggregate memory writes
  SELECT COUNT(*)
    INTO v_total_mem_writes
    FROM agents.memories
   WHERE team_run_id = p_team_run_id
     AND source IN ('agent','tool');

  -- Latest completed evaluation score for the workflow
  SELECT er.score
    INTO v_eval_score
    FROM agents.evaluation_runs er
    JOIN agents.evaluations e ON e.id = er.evaluation_id
   WHERE e.target_type = 'workflow'
     AND e.target_id   = v_run.workflow_id
     AND er.status     = 'completed'
   ORDER BY er.completed_at DESC NULLS LAST
   LIMIT 1;

  -- Detect 'killed': cancelled + kill_switch policy denial
  IF EXISTS (
    SELECT 1
      FROM agents.policy_evaluations
     WHERE team_run_id = p_team_run_id
       AND policy_type = 'kill_switch'
       AND verdict     = 'deny'
  ) AND v_run.status = 'cancelled' THEN
    v_outcome := 'killed';
  ELSE
    CASE v_run.status
      WHEN 'completed' THEN v_outcome := 'success';
      WHEN 'failed'    THEN v_outcome := 'failed';
      WHEN 'cancelled' THEN v_outcome := 'cancelled';
      ELSE                  v_outcome := 'partial';
    END CASE;
  END IF;

  -- Title
  SELECT 'Run Report: ' || COALESCE(w.title, p_team_run_id::text)
    INTO v_title
    FROM lenses.workflows w
   WHERE w.id = v_run.workflow_id;

  IF v_title IS NULL THEN
    v_title := 'Run Report: ' || p_team_run_id::text;
  END IF;

  -- Step status breakdown
  SELECT COALESCE(jsonb_object_agg(status, cnt), '{}'::jsonb)
    INTO v_step_statuses
    FROM (
      SELECT status, COUNT(*) AS cnt
        FROM agents.agent_run_steps
       WHERE team_run_id = p_team_run_id
       GROUP BY status
    ) s;

  -- Tool status breakdown
  SELECT COALESCE(jsonb_object_agg(status, cnt), '{}'::jsonb)
    INTO v_tool_statuses
    FROM (
      SELECT status, COUNT(*) AS cnt
        FROM agents.tool_invocations
       WHERE team_run_id = p_team_run_id
       GROUP BY status
    ) t;

  -- Duration
  IF v_run.started_at IS NOT NULL AND v_run.completed_at IS NOT NULL THEN
    v_duration_secs := EXTRACT(EPOCH FROM (v_run.completed_at - v_run.started_at));
  ELSE
    v_duration_secs := NULL;
  END IF;

  -- Compose metrics
  v_metrics := jsonb_build_object(
    'step_statuses',  v_step_statuses,
    'tool_statuses',  v_tool_statuses,
    'duration_seconds', v_duration_secs
  );

  INSERT INTO agents.run_reports (
    ai_lenser_id,
    team_run_id,
    workflow_run_id,
    title,
    metrics,
    total_steps,
    total_tool_invocations,
    total_memory_writes,
    total_cost_estimate,
    evaluation_score,
    outcome
  ) VALUES (
    v_run.ai_lenser_id,
    p_team_run_id,
    v_run.workflow_run_id,
    v_title,
    v_metrics,
    COALESCE(v_total_steps, 0),
    COALESCE(v_total_tools, 0),
    COALESCE(v_total_mem_writes, 0),
    COALESCE(v_total_cost, 0),
    v_eval_score,
    v_outcome
  )
  RETURNING id INTO v_report_id;

  RETURN v_report_id;
END;
$$;


ALTER FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[] DEFAULT ARRAY[]::"date"[], "p_timezone" "text" DEFAULT 'UTC'::"text") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  INSERT INTO lenses.schedule_calendars
    (lenser_id, name, kind, dates, timezone, is_seed)
  VALUES
    (lensers.get_auth_lenser_id(), p_name, p_kind, p_dates, p_timezone, false)
  RETURNING id;
$$;


ALTER FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") IS 'Security wrapper: create a schedule calendar owned by the current user. lenser_id is derived from the session — callers cannot supply it.';



CREATE OR REPLACE FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "agents"."scratchpad_runs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_actor uuid := auth.uid();
  v_row   agents.scratchpad_runs;
BEGIN
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
  END IF;
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.scratchpad_runs (ai_lenser_id, actor_lenser_id, prompt, model_id, metadata)
  VALUES (p_ai_lenser_id, v_actor, p_prompt, p_model_id, COALESCE(p_metadata, '{}'::jsonb))
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text" DEFAULT 'en'::"text") RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "visibility" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
DECLARE
  v_tag_id uuid;
BEGIN
  IF btrim(p_name) = '' OR p_name IS NULL THEN
    RAISE EXCEPTION 'Tag name is required';
  END IF;
  IF btrim(p_slug) = '' OR p_slug IS NULL THEN
    RAISE EXCEPTION 'Tag slug is required';
  END IF;

  -- Idempotent insert of base tag
  INSERT INTO content.tags (slug, visibility)
  VALUES (btrim(p_slug), 'public')
  ON CONFLICT ON CONSTRAINT tags_slug_key DO NOTHING;

  -- Resolve tag id (handles both new insert and pre-existing slug)
  SELECT t.id INTO v_tag_id
  FROM content.tags t
  WHERE t.slug = btrim(p_slug);

  IF v_tag_id IS NULL THEN
    RAISE EXCEPTION 'Failed to resolve tag for slug: %', p_slug;
  END IF;

  -- Idempotent insert of translation for the requested language
  INSERT INTO content.tag_translations (tag_id, language_code, name)
  VALUES (v_tag_id, btrim(p_language_code), btrim(p_name))
  ON CONFLICT ON CONSTRAINT tag_translations_tag_id_language_id_key DO NOTHING;

  -- Return resolved row from the public view
  RETURN QUERY
    SELECT v.id, v.slug, v.name, v.visibility
    FROM public.vw_tags_public_stats v
    WHERE v.id = v_tag_id;
END;
$$;


ALTER FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_run_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_assignment_id" "uuid" DEFAULT NULL::"uuid", "p_team_id" "uuid" DEFAULT NULL::"uuid", "p_approval_status" "text" DEFAULT 'pending'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  INSERT INTO agents.team_runs
    (ai_lenser_id, workflow_id, workflow_run_id, workflow_assignment_id,
     team_id, status, approval_status, started_at)
  VALUES
    (p_ai_lenser_id, p_workflow_id, p_workflow_run_id, p_workflow_assignment_id,
     p_team_id, 'running', p_approval_status, now())
  RETURNING to_jsonb(agents.team_runs.*) INTO v_result;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  INSERT INTO content.thread_replies (thread_id, lenser_id, content, parent_reply_id)
  VALUES (p_thread_id, lensers.get_auth_lenser_id(), p_content, p_parent_reply_id)
  RETURNING id, lenser_id;
$$;


ALTER FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") IS 'Security wrapper: insert a reply into a thread as the current user. Returns the new reply id and lenser_id.';



CREATE TABLE IF NOT EXISTS "battles"."tournaments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "title" "text" NOT NULL,
    "slug" "text" NOT NULL,
    "creator_lenser_id" "uuid",
    "format" "text" DEFAULT 'single_elimination'::"text" NOT NULL,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "max_contenders" integer DEFAULT 8 NOT NULL,
    "battle_type" "text" DEFAULT 'ai_vs_ai'::"text" NOT NULL,
    "ai_judge_enabled" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tournaments_format_check" CHECK (("format" = ANY (ARRAY['single_elimination'::"text", 'round_robin'::"text", 'swiss'::"text"]))),
    CONSTRAINT "tournaments_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'registration'::"text", 'active'::"text", 'completed'::"text", 'cancelled'::"text"])))
);


ALTER TABLE "battles"."tournaments" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text" DEFAULT 'single_elimination'::"text", "p_max_contenders" integer DEFAULT 8, "p_battle_type" "text" DEFAULT 'ai_vs_ai'::"text", "p_ai_judge_enabled" boolean DEFAULT false) RETURNS "battles"."tournaments"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_tournament battles.tournaments;
  v_slug       TEXT;
BEGIN
  v_slug := lower(regexp_replace(p_title, '[^a-zA-Z0-9]', '-', 'g')) || '-' || to_char(now(), 'YYYYMMDD-HH24MI');

  INSERT INTO battles.tournaments
    (title, slug, creator_lenser_id, format, status, max_contenders, battle_type, ai_judge_enabled)
  VALUES
    (p_title, v_slug, auth.uid(), p_format, 'registration', p_max_contenders, p_battle_type, p_ai_judge_enabled)
  RETURNING * INTO v_tournament;

  RETURN v_tournament;
END;
$$;


ALTER FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_visibility" "text" DEFAULT 'public'::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthorized' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO lenses.workflows (lenser_id, title, description, visibility)
  VALUES (v_lenser_id, p_title, p_description, p_visibility)
  RETURNING
    lenses.workflows.id,
    lenses.workflows.lenser_id,
    lenses.workflows.title,
    lenses.workflows.description,
    lenses.workflows.visibility,
    lenses.workflows.battle_count,
    lenses.workflows.created_at,
    lenses.workflows.updated_at;
END;
$$;


ALTER FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") IS 'Creates a workflow. Lenser identity is resolved from JWT via get_auth_lenser_id() — p_lenser_id was removed to prevent identity spoofing.';



CREATE OR REPLACE FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN lenses.fn_create_workflow_version(p_workflow_id, p_changelog);
END;
$$;


ALTER FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") IS 'Public wrapper: snapshots the current workflow DAG into a new draft version. Owner-only. Delegates ownership/auth check to lenses.fn_create_workflow_version.';



CREATE OR REPLACE FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_result    jsonb;
  v_data      jsonb := COALESCE(p_data, '{}'::jsonb);
  v_cols      text;
  v_selects   text;
BEGIN
  IF p_table_name NOT IN (
    'personality_profiles', 'memory_profiles', 'tool_profiles', 'model_profiles',
    'workflow_assignments', 'evaluation_cases', 'evaluation_rubrics',
    'evaluation_baselines', 'tools_registry', 'evaluations',
    'team_members', 'team_edges', 'agent_run_events', 'agent_run_steps'
  ) THEN
    RAISE EXCEPTION 'create_forbidden: table % not in allowlist', p_table_name
      USING ERRCODE = '42501';
  END IF;

  -- Drop fields whose column defaults should fire when the caller omits a
  -- value. Today these are id, created_at, updated_at. Stripping them keeps
  -- the INSERT column list lean so DEFAULT clauses apply.
  v_data := v_data - 'id' - 'created_at' - 'updated_at';

  IF jsonb_typeof(v_data) <> 'object' THEN
    RAISE EXCEPTION 'create_forbidden: p_data must be a JSON object' USING ERRCODE = '22023';
  END IF;

  -- Empty payloads: rely entirely on column defaults / NOT NULL violations
  -- (the caller did not specify any fields).
  IF (SELECT count(*) FROM jsonb_object_keys(v_data)) = 0 THEN
    EXECUTE format(
      'INSERT INTO agents.%I DEFAULT VALUES RETURNING to_jsonb(%I.*)',
      p_table_name, p_table_name
    ) INTO v_result;
    RETURN v_result;
  END IF;

  -- Build the column list and the matching SELECT-from-record list from the
  -- keys actually present in v_data. jsonb_populate_record casts each value
  -- to the column's true type; we then project only those columns so the
  -- omitted ones fall back to their DEFAULT clause on INSERT.
  SELECT
    string_agg(quote_ident(k), ', '            ORDER BY k),
    string_agg(format('r.%I', k), ', '         ORDER BY k)
  INTO v_cols, v_selects
  FROM jsonb_object_keys(v_data) AS k;

  EXECUTE format(
    'INSERT INTO agents.%I (%s) '
    'SELECT %s FROM jsonb_populate_record(NULL::agents.%I, %L::jsonb) AS r '
    'RETURNING to_jsonb(%I.*)',
    p_table_name,
    v_cols,
    v_selects,
    p_table_name,
    v_data::text,
    p_table_name
  ) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") IS 'Create an agents workspace record. Strips id/created_at/updated_at from the JSONB payload so column defaults (gen_random_uuid, now) fire. Inserts only the columns the caller actually provided. Whitelisted on p_table_name.';



CREATE OR REPLACE FUNCTION "public"."fn_deactivate_account"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_profile RECORD;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT id, user_id, status
  INTO v_profile
  FROM lensers.profiles
  WHERE user_id = v_uid
  FOR UPDATE;

  IF NOT FOUND OR v_profile.status <> 'active' THEN
    RETURN jsonb_build_object('success', false, 'reason', 'not_active');
  END IF;

  UPDATE lensers.profiles
  SET status = 'deactivated',
      updated_at = now()
  WHERE id = v_profile.id;

  PERFORM lensers.log_account_lifecycle_event(
    v_profile.id,
    v_profile.user_id,
    'account_deactivated',
    v_profile.status,
    'deactivated'::lensers.lenser_status,
    'user_action',
    jsonb_build_object('reason', 'user_requested_deactivation')
  );

  RETURN jsonb_build_object('success', true, 'status', 'deactivated');
END;
$$;


ALTER FUNCTION "public"."fn_deactivate_account"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text" DEFAULT NULL::"text", "p_modifications" "jsonb" DEFAULT NULL::"jsonb") RETURNS TABLE("request_id" "uuid", "ai_lenser_id" "uuid", "team_id" "uuid", "workflow_id" "uuid", "workflow_run_id" "uuid", "workflow_assignment_id" "uuid", "approval_status" "text", "run_status" "text", "metadata" "jsonb", "decided_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lenses', 'lensers'
    AS $$
DECLARE
  v_actor          uuid;
  v_existing       agents.team_runs%ROWTYPE;
  v_decision_at    timestamptz := now();
  v_event_type     text;
  v_new_metadata   jsonb;
  v_terminal_run   text;
BEGIN
  IF p_decision NOT IN ('approved', 'rejected', 'modified') THEN
    RAISE EXCEPTION 'Invalid decision %. Allowed: approved | rejected | modified.', p_decision
      USING ERRCODE = '22023';
  END IF;

  IF p_decision = 'modified' AND (p_modifications IS NULL OR jsonb_typeof(p_modifications) <> 'object') THEN
    RAISE EXCEPTION 'modify-and-approve requires a non-null jsonb object for p_modifications.'
      USING ERRCODE = '22023';
  END IF;

  v_actor := lensers.get_auth_human_lenser_id();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_existing
  FROM agents.team_runs
  WHERE id = p_team_run_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Approval request % not found.', p_team_run_id
      USING ERRCODE = '42501';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_existing.ai_lenser_id) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI workspace.'
      USING ERRCODE = '42501';
  END IF;

  IF v_existing.approval_status <> 'pending' THEN
    RAISE EXCEPTION 'Request % is in approval_status="%", cannot decide twice.',
      p_team_run_id, v_existing.approval_status
      USING ERRCODE = '22023';
  END IF;

  v_new_metadata := COALESCE(v_existing.metadata, '{}'::jsonb)
    || jsonb_build_object(
         'decision_at', v_decision_at,
         'decision_by_lenser_id', v_actor,
         'decision_reason', p_reason
       );

  IF p_decision = 'modified' THEN
    v_new_metadata := v_new_metadata
      || jsonb_build_object('decision_modifications', p_modifications);
    v_event_type := 'approval_modified';
  ELSIF p_decision = 'approved' THEN
    v_event_type := 'approval_granted';
  ELSE
    v_event_type := 'approval_rejected';
  END IF;

  -- Approved-or-modified resumes the run by flipping team_run + workflow_run
  -- status. Rejected terminates both with `failed`.
  IF p_decision IN ('approved', 'modified') THEN
    UPDATE agents.team_runs
    SET approval_status = CASE WHEN p_decision = 'modified' THEN 'approved' ELSE p_decision END,
        status = 'queued',
        metadata = v_new_metadata,
        updated_at = v_decision_at
    WHERE id = p_team_run_id;

    IF v_existing.workflow_run_id IS NOT NULL THEN
      -- Recovery sweeper picks up `queued` runs; we do not call the engine
      -- directly. Only flip out of `blocked` so we never override an active
      -- transition that another process may have written.
      UPDATE lenses.workflow_runs
      SET status = 'queued'
      WHERE id = v_existing.workflow_run_id
        AND status IN ('pending', 'queued');
    END IF;

    v_terminal_run := 'queued';
  ELSE
    UPDATE agents.team_runs
    SET approval_status = 'rejected',
        status = 'failed',
        completed_at = v_decision_at,
        metadata = v_new_metadata,
        updated_at = v_decision_at
    WHERE id = p_team_run_id;

    IF v_existing.workflow_run_id IS NOT NULL THEN
      UPDATE lenses.workflow_runs
      SET status = 'failed',
          completed_at = COALESCE(completed_at, v_decision_at)
      WHERE id = v_existing.workflow_run_id
        AND status NOT IN ('completed', 'cancelled', 'failed');
    END IF;

    v_terminal_run := 'failed';
  END IF;

  INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
  VALUES (
    p_team_run_id,
    v_event_type,
    jsonb_build_object(
      'decision', p_decision,
      'reason', p_reason,
      'modifications', p_modifications,
      'decided_by_lenser_id', v_actor,
      'previous_run_status', v_existing.status,
      'next_run_status', v_terminal_run
    ),
    v_decision_at
  );

  RETURN QUERY
  SELECT
    p_team_run_id                                               AS request_id,
    v_existing.ai_lenser_id                                     AS ai_lenser_id,
    v_existing.team_id                                          AS team_id,
    v_existing.workflow_id                                      AS workflow_id,
    v_existing.workflow_run_id                                  AS workflow_run_id,
    v_existing.workflow_assignment_id                           AS workflow_assignment_id,
    CASE WHEN p_decision IN ('approved', 'modified') THEN 'approved' ELSE 'rejected' END AS approval_status,
    v_terminal_run                                              AS run_status,
    v_new_metadata                                              AS metadata,
    v_decision_at                                               AS decided_at;
END;
$$;


ALTER FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") IS 'Atomic owner-decision RPC for approval gates. Writes approval_status, merges decision audit fields into metadata, appends an agent_run_events row, and transitions the underlying workflow_run.';



CREATE OR REPLACE FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'audit', 'lensers'
    AS $$
DECLARE
  v_lenser_id        uuid;
  v_creator_lenser_id uuid;
  v_decision_type    text;
  v_new_status       battles.submission_status_enum;
BEGIN
  IF p_decision NOT IN ('allow', 'reject') THEN
    RAISE EXCEPTION 'Invalid decision: must be ''allow'' or ''reject''';
  END IF;

  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  -- Verify caller is the battle creator
  SELECT creator_lenser_id INTO v_creator_lenser_id
  FROM battles.battles
  WHERE id = p_battle_id;

  IF v_creator_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Battle not found: %', p_battle_id;
  END IF;

  IF v_creator_lenser_id <> v_lenser_id THEN
    RAISE EXCEPTION 'Only the battle creator may override moderation decisions'
      USING ERRCODE = '42501';
  END IF;

  -- Map decision to enum values
  IF p_decision = 'allow' THEN
    v_decision_type := 'restored';
    v_new_status    := 'submitted';
  ELSE
    v_decision_type := 'rejected';
    v_new_status    := 'disqualified';
  END IF;

  -- Update the submission status
  UPDATE battles.submissions
  SET status = v_new_status,
      updated_at = now()
  WHERE id = p_entry_id
    AND battle_id = p_battle_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Submission not found: %', p_entry_id;
  END IF;

  -- Audit the decision
  INSERT INTO audit.moderation_decisions (
    target_entity_schema,
    target_entity_table,
    target_entity_id,
    decision_type,
    reason,
    moderator_lenser_id,
    is_ai_moderated
  ) VALUES (
    'battles',
    'submissions',
    p_entry_id,
    v_decision_type,
    p_reason,
    v_lenser_id,
    false
  );
END;
$$;


ALTER FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'lenses', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_auth_lenser_id uuid;
  v_log            platform.tool_invocation_logs%ROWTYPE;
BEGIN
  IF p_decision NOT IN ('approved', 'rejected') THEN
    RAISE EXCEPTION 'Invalid decision "%" — must be ''approved'' or ''rejected''', p_decision
      USING ERRCODE = '22023';
  END IF;

  v_auth_lenser_id := lensers.get_auth_lenser_id();

  SELECT * INTO v_log
  FROM platform.tool_invocation_logs
  WHERE id = p_log_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'tool_invocation_log % not found', p_log_id
      USING ERRCODE = 'P0002';
  END IF;

  -- Caller must own the ai_lenser that produced this log entry
  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    WHERE al.id = v_log.ai_lenser_id
      AND al.profile_id = v_auth_lenser_id
  ) THEN
    RAISE EXCEPTION 'Not authorised to decide on tool invocation log %', p_log_id
      USING ERRCODE = '42501';
  END IF;

  IF v_log.approval_status <> 'awaiting_approval' THEN
    RAISE EXCEPTION 'Cannot decide on log % with status "%"', p_log_id, v_log.approval_status
      USING ERRCODE = '22023';
  END IF;

  UPDATE platform.tool_invocation_logs
  SET
    approval_status = p_decision,
    decided_at      = now(),
    decided_by      = v_auth_lenser_id
  WHERE id = p_log_id;

  -- Emit a tool_decision event into the run timeline if the log is tied to a run
  IF v_log.run_id IS NOT NULL THEN
    INSERT INTO lenses.workflow_run_events (run_id, type, payload)
    VALUES (
      v_log.run_id,
      'tool_decision',
      jsonb_build_object(
        'log_id',    p_log_id,
        'tool_name', v_log.tool_name,
        'decision',  p_decision,
        'reason',    p_reason,
        'decided_by', v_auth_lenser_id,
        'decided_at', now()
      )
    );
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") IS 'Records approve/reject decision on platform.tool_invocation_logs and emits a tool_decision event to lenses.workflow_run_events for run timeline visibility.';



CREATE OR REPLACE FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
declare
  v_lenser_id uuid;
  v_encrypted bytea;
  v_key bytea;
  v_decrypted text;
begin
  -- Verify ownership
  select lenser_id, encrypted_config
  into v_lenser_id, v_encrypted
  from public.workflow_integration_credentials
  where id = p_credential_id;

  if v_lenser_id is null then
    raise exception 'Credential not found';
  end if;

  if v_lenser_id != (select id from lensers.profiles where user_id = auth.uid()) then
    raise exception 'Access denied';
  end if;

  -- Decrypt using the platform encryption key from vault
  select decrypted_secret into v_key
  from vault.decrypted_secrets
  where name = 'workflow_integration_encryption_key'
  limit 1;

  if v_key is null then
    raise exception 'Encryption key not configured';
  end if;

  v_decrypted := pgp_sym_decrypt(v_encrypted, encode(v_key, 'hex'));
  return v_decrypted::jsonb;
end;
$$;


ALTER FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_delete('agent', p_ai_lenser_id); $$;


ALTER FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  DELETE FROM automation.trigger_rules
  WHERE  id        = p_rule_id
    AND  lenser_id = auth.uid();
$$;


ALTER FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") IS 'Security wrapper: delete a trigger rule owned by the current user. Silently no-ops if the rule does not belong to the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_delete('lens', p_lens_id);
END;
$$;


ALTER FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
DECLARE
  v_owner_id UUID;
BEGIN
  SELECT owner_lenser_id INTO v_owner_id
  FROM media.objects
  WHERE id = p_object_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'media_object_not_found: %', p_object_id USING ERRCODE = 'P0001';
  END IF;

  IF v_owner_id != auth.uid() THEN
    RAISE EXCEPTION 'media_delete_forbidden: caller does not own object %', p_object_id
      USING ERRCODE = 'P0001';
  END IF;

  UPDATE media.objects
  SET lifecycle_state = 'deleted'
  WHERE id = p_object_id;

  INSERT INTO audit.events (event_type, actor_type, severity, payload)
  VALUES (
    'media.object_deleted',
    'user',
    'info',
    jsonb_build_object('object_id', p_object_id, 'deleted_by', auth.uid(), 'deleted_at', now())
  );
END;
$$;


ALTER FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") IS 'AT: Soft-deletes a media object (lifecycle_state=deleted). Owner only. Writes audit.events row. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  DELETE FROM content.threads
  WHERE id        = p_thread_id
    AND lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") IS 'Security wrapper: delete a thread owned by the current user. Silently no-ops if not owned.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  DELETE FROM content.thread_replies
  WHERE id        = p_reply_id
    AND lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") IS 'Security wrapper: delete a thread reply owned by the current user. Silently no-ops if not owned.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_delete('workflow', p_workflow_id); $$;


ALTER FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflow_edges e
    JOIN lenses.workflows w ON w.id = e.workflow_id
    WHERE e.id = p_edge_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized or edge not found: %', p_edge_id
      USING ERRCODE = '42501';
  END IF;

  DELETE FROM lenses.workflow_edges WHERE id = p_edge_id;
END;
$$;


ALTER FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") IS 'Deletes a workflow edge the caller owns.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflow_nodes n
    JOIN lenses.workflows w ON w.id = n.workflow_id
    WHERE n.id = p_node_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized or node not found: %', p_node_id
      USING ERRCODE = '42501';
  END IF;

  DELETE FROM lenses.workflow_nodes WHERE id = p_node_id;
END;
$$;


ALTER FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") IS 'Deletes a workflow node the caller owns.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  DELETE FROM lenses.workflow_phases wp
  USING lenses.workflows w
  WHERE wp.id = p_phase_id
    AND wp.workflow_id = w.id
    AND w.lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  DELETE FROM lenses.workflow_schedules
  WHERE id        = p_schedule_id
    AND workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") IS 'Security wrapper: delete a workflow schedule owned by the current user. Silently no-ops if not found or not owned by caller.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  DELETE FROM lenses.workflow_tasks wt
  USING lenses.workflows w
  WHERE wt.id = p_task_id
    AND wt.workflow_id = w.id
    AND w.lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $_$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
  v_predicate text;
BEGIN
  IF p_table_name NOT IN (
    'teams', 'team_members', 'team_edges', 'personality_profiles',
    'memory_profiles', 'tool_profiles', 'model_profiles',
    'workflow_assignments', 'evaluation_cases', 'evaluation_rubrics',
    'evaluation_baselines', 'tools_registry', 'tool_assignments',
    'evaluations', 'scratchpad_runs', 'workspace_settings',
    'agent_run_events', 'agent_run_steps'
  ) THEN
    RAISE EXCEPTION 'delete_forbidden: table % not in allowlist', p_table_name
      USING ERRCODE = '42501';
  END IF;

  v_predicate := public.fn__workspace_ownership_predicate(p_table_name);
  IF v_predicate IS NULL THEN
    RAISE EXCEPTION 'delete_forbidden: no ownership predicate for %', p_table_name
      USING ERRCODE = '42501';
  END IF;

  EXECUTE format(
    'DELETE FROM agents.%I tbl WHERE tbl.id = $1 AND (%s OR public.is_admin())',
    p_table_name,
    v_predicate
  ) USING p_id, v_lenser_id;
END;
$_$;


ALTER FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") IS 'Delete an agents workspace record. Ownership: ai_lenser_id via agents.ownerships, or team_id via teams→ownerships, or is_admin. No reference to non-existent owner_lenser_id column.';



CREATE OR REPLACE FUNCTION "public"."fn_deny_mutation"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'public'
    AS $$
BEGIN
  RAISE EXCEPTION '% is append-only and cannot be %', TG_TABLE_NAME, TG_OP;
END;
$$;


ALTER FUNCTION "public"."fn_deny_mutation"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_deny_mutation"() IS 'Generic append-only guard. Attach as BEFORE UPDATE/DELETE trigger on any table that must be insert-only (rule_snapshots, action_logs, audit chains, etc.). Raises an exception on any mutation attempt.';



CREATE OR REPLACE FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'auth', 'public'
    AS $$
DECLARE
  rec RECORD;
BEGIN
  -- Pattern: new account (<24h old)
  FOR rec IN
    SELECT DISTINCT v.voter_lenser_id
      FROM battles.votes v
      JOIN lensers.profiles p ON p.id = v.voter_lenser_id
      JOIN auth.users u ON u.id = p.user_id
     WHERE v.battle_id = p_battle_id
       AND u.created_at >= now() - INTERVAL '24 hours'
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'new_account',
      'score',    0.7
    );
  END LOOP;

  -- Pattern: 0 battles participated (voter never submitted an entry)
  FOR rec IN
    SELECT DISTINCT v.voter_lenser_id
      FROM battles.votes v
     WHERE v.battle_id = p_battle_id
       AND NOT EXISTS (
         SELECT 1 FROM battles.contenders c
          WHERE c.contender_ref_id = v.voter_lenser_id
       )
       AND NOT EXISTS (
         SELECT 1 FROM battles.votes v2
          WHERE v2.voter_lenser_id = v.voter_lenser_id
            AND v2.battle_id <> p_battle_id
       )
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'zero_battles_participated',
      'score',    0.5
    );
  END LOOP;

  -- Pattern: >80% votes to single contender
  FOR rec IN
    SELECT
      v.voter_lenser_id,
      MAX(cnt)::float / NULLIF(SUM(cnt)::float, 0) AS concentration
    FROM (
      SELECT voter_lenser_id, voted_contender_id, COUNT(*) AS cnt
        FROM battles.votes
       WHERE battle_id = p_battle_id
       GROUP BY voter_lenser_id, voted_contender_id
    ) v
    GROUP BY v.voter_lenser_id
    HAVING MAX(cnt)::float / NULLIF(SUM(cnt)::float, 0) > 0.8
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'high_concentration',
      'score',    LEAST(1.0, rec.concentration)
    );
  END LOOP;

  -- Pattern: votes spaced <2s apart
  FOR rec IN
    SELECT DISTINCT v1.voter_lenser_id
      FROM battles.votes v1
      JOIN battles.votes v2
        ON v2.voter_lenser_id = v1.voter_lenser_id
       AND v2.battle_id       = v1.battle_id
       AND v2.id             <> v1.id
       AND ABS(EXTRACT(EPOCH FROM (v2.created_at - v1.created_at))) < 2
     WHERE v1.battle_id = p_battle_id
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'rapid_succession',
      'score',    0.8
    );
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") IS 'CA: Emits anomaly signals for a battle. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_approve(p_device_id);
$$;


ALTER FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text" DEFAULT NULL::"text", "p_envelope_sig" "text" DEFAULT NULL::"text", "p_gateway_status" "text" DEFAULT 'connected'::"text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_heartbeat(
    p_device_id, p_daemon_version, p_envelope_sig, p_gateway_status
  );
$$;


ALTER FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_list"("p_trust_level" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50, "p_cursor" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "name" "text", "device_type" "text", "os" "text", "arch" "text", "cli_version" "text", "runner_version" "text", "capabilities" "jsonb", "trust_level" "text", "gateway_status" "text", "last_seen_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_device_list(p_trust_level, p_limit, p_cursor);
$$;


ALTER FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone DEFAULT "now"()) RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_post_challenge(p_device_id, p_signature, p_signed_iat);
$$;


ALTER FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text" DEFAULT 'other'::"text", "p_os" "text" DEFAULT NULL::"text", "p_arch" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_capabilities" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_register(
    p_name, p_device_type, p_os, p_arch, p_cli_version, p_capabilities
  );
$$;


ALTER FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text" DEFAULT 'other'::"text", "p_os" "text" DEFAULT NULL::"text", "p_arch" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_daemon_version" "text" DEFAULT NULL::"text", "p_capabilities" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("device_id" "uuid", "challenge_id" "uuid", "challenge_nonce" "text", "challenge_expires_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_device_register_with_key(
    p_name, p_public_key, p_device_type, p_os, p_arch,
    p_cli_version, p_daemon_version, p_capabilities
  );
$$;


ALTER FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_revoke(p_device_id);
$$;


ALTER FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'platform', 'public'
    AS $$
DECLARE
  v_dispatch_enabled boolean;
  v_result           integer;
BEGIN
  -- Check platform-level autonomy kill switch (populated by Phase 2.9 migration).
  -- If the table or row does not yet exist, default to enabled.
  BEGIN
    SELECT (value::boolean) INTO v_dispatch_enabled
    FROM platform.system_flags
    WHERE key = 'autonomy_dispatch_enabled';
  EXCEPTION WHEN undefined_table OR undefined_column THEN
    v_dispatch_enabled := true;
  END;

  IF v_dispatch_enabled IS NOT DISTINCT FROM false THEN
    RAISE NOTICE 'Autonomous dispatch is disabled (platform.system_flags.autonomy_dispatch_enabled=false). Skipping.';
    RETURN 0;
  END IF;

  -- requiresApproval=false AND spending_limit_credits IS NULL guard:
  -- Log a warning for any schedule that bypasses approval without a spending cap.
  -- The UPSERT guard above should prevent these from being created, but we
  -- audit here as a belt-and-suspenders check.
  PERFORM 1
  FROM lenses.workflow_schedules s
  JOIN lenses.workflows w ON w.id = s.workflow_id
  JOIN agents.ai_lensers al ON al.profile_id = w.lenser_id
  JOIN agents.policies pol ON pol.id = al.policy_id
  WHERE s.is_active = true
    AND COALESCE((s.approval_policy->>'requiresApproval')::boolean, true) = false
    AND pol.spending_limit_credits IS NULL;

  IF FOUND THEN
    RAISE WARNING
      'One or more active schedules have requiresApproval=false and no spending_limit_credits. '
      'These schedules will dispatch without approval gates and without a cost ceiling. '
      'Set a spending limit or re-enable approval to suppress this warning.';
  END IF;

  -- Delegate to the existing approval-aware dispatcher (lenses schema).
  SELECT lenses.fn_dispatch_scheduled_workflows() INTO v_result;
  RETURN COALESCE(v_result, 0);
END;
$$;


ALTER FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() IS 'Phase 2 pg_cron target. Wraps lenses.fn_dispatch_scheduled_workflows() with a platform-level autonomy kill switch check and an approval/spending-limit audit warning. The kill switch is controlled via platform.system_flags.autonomy_dispatch_enabled.';



CREATE OR REPLACE FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean DEFAULT false) RETURNS TABLE("switch_id" "uuid", "cancelled_runs" integer, "cancelled_jobs" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_switch_id UUID;
  v_run_count INT := 0;
  v_job_count INT := 0;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'emergency_stop_forbidden: admin role required';
  END IF;

  -- Activate a system-wide kill switch. fn_kill_switch_activate is SECURITY DEFINER
  -- and checks is_admin() internally; it also writes its own audit.events row.
  v_switch_id := public.fn_kill_switch_activate('system', NULL, p_reason, NULL);

  -- Optional: mass-cancel all in-flight runs and jobs.
  IF p_force_mode THEN
    SELECT c.cancelled_run_count, c.cancelled_job_count
    INTO   v_run_count, v_job_count
    FROM   public.fn_cancel_all_active_runs(p_reason) c;
  END IF;

  RETURN QUERY SELECT v_switch_id, v_run_count, v_job_count;
END;
$$;


ALTER FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) IS 'Admin-only emergency stop. Activates a system-wide kill switch that blocks all worker claim and schedule dispatch calls. When force_mode=true, also calls fn_cancel_all_active_runs to cancel in-flight runs and battle jobs immediately. Returns (switch_id, cancelled_runs, cancelled_jobs).';



CREATE OR REPLACE FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("verdict" "text", "reason" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_settings        agents.workspace_settings;
  v_credits_used    numeric;
  v_active_runs     integer;
  v_workflow_hash   integer;
  v_verdict         text;
  v_reason          text;
  v_policy_type     text;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  -- Load or default settings
  SELECT * INTO v_settings
    FROM agents.workspace_settings
   WHERE ai_lenser_id = p_ai_lenser_id;

  IF NOT FOUND THEN
    -- All defaults: allow
    v_verdict     := 'allow';
    v_reason      := NULL;
    v_policy_type := 'kill_switch';

    INSERT INTO agents.policy_evaluations (
      ai_lenser_id, team_run_id, tool_invocation_id,
      evaluation_point, policy_type, verdict, reason, context, evaluated_at
    ) VALUES (
      p_ai_lenser_id, NULL, NULL,
      'pre_run', v_policy_type, v_verdict, v_reason,
      COALESCE(p_context, '{}'::jsonb), now()
    );

    RETURN QUERY SELECT v_verdict, v_reason;
    RETURN;
  END IF;

  -- 1. Global kill switch
  IF v_settings.global_kill_switch THEN
    v_verdict     := 'deny';
    v_reason      := 'global kill switch active';
    v_policy_type := 'kill_switch';

  -- 2. Agent paused
  ELSIF v_settings.agent_paused THEN
    v_verdict     := 'pause';
    v_reason      := 'agent is paused';
    v_policy_type := 'pause';

  -- 3. Daily budget
  ELSIF v_settings.budget_enforce THEN
    SELECT COALESCE(SUM(credits_spent), 0)
      INTO v_credits_used
      FROM agents.quota_snapshots
     WHERE ai_lenser_id = p_ai_lenser_id
       AND period_date  = CURRENT_DATE;

    IF v_credits_used >= v_settings.max_daily_credits THEN
      v_verdict     := 'deny';
      v_reason      := 'daily budget exceeded';
      v_policy_type := 'budget';
    END IF;
  END IF;

  -- 4. Parallel run limit (only if not already denied above)
  IF v_verdict IS NULL THEN
    SELECT COUNT(*)
      INTO v_active_runs
      FROM agents.team_runs
     WHERE ai_lenser_id = p_ai_lenser_id
       AND status IN ('queued','running','blocked');

    IF v_active_runs >= v_settings.max_parallel_runs THEN
      v_verdict     := 'deny';
      v_reason      := 'max_parallel_runs limit reached';
      v_policy_type := 'parallel_limit';
    END IF;
  END IF;

  -- 5. Dark launch gating
  IF v_verdict IS NULL
     AND v_settings.dark_launch_enabled
     AND p_workflow_id IS NOT NULL
  THEN
    v_workflow_hash := (('x' || md5(p_workflow_id::text))::bit(32)::int & 2147483647) % 100;
    IF v_workflow_hash >= v_settings.dark_launch_pct THEN
      v_verdict     := 'deny';
      v_reason      := 'outside dark launch percentage';
      v_policy_type := 'dark_launch';
    END IF;
  END IF;

  -- 6. Approval default: require_human
  IF v_verdict IS NULL AND v_settings.approval_default = 'require_human' THEN
    v_verdict     := 'require_approval';
    v_reason      := 'workspace default requires human approval';
    v_policy_type := 'approval';
  END IF;

  -- 7. Approval default: deny-all
  IF v_verdict IS NULL AND v_settings.approval_default = 'deny' THEN
    v_verdict     := 'deny';
    v_reason      := 'workspace approval policy is deny-all';
    v_policy_type := 'approval';
  END IF;

  -- 8. Default: allow
  IF v_verdict IS NULL THEN
    v_verdict     := 'allow';
    v_reason      := NULL;
    v_policy_type := 'kill_switch';
  END IF;

  -- Always audit
  INSERT INTO agents.policy_evaluations (
    ai_lenser_id, team_run_id, tool_invocation_id,
    evaluation_point, policy_type, verdict, reason, context, evaluated_at
  ) VALUES (
    p_ai_lenser_id, NULL, NULL,
    'pre_run', v_policy_type, v_verdict, v_reason,
    COALESCE(p_context, '{}'::jsonb), now()
  );

  RETURN QUERY SELECT v_verdict, v_reason;
END;
$$;


ALTER FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text" DEFAULT 'succeeded'::"text", "p_response_text" "text" DEFAULT NULL::"text", "p_response_meta" "jsonb" DEFAULT '{}'::"jsonb", "p_token_input" integer DEFAULT 0, "p_token_output" integer DEFAULT 0, "p_credit_cost" bigint DEFAULT 0) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
DECLARE
    v_request_id     uuid;
    v_run_id         uuid;
    v_model_id       uuid;
    v_billing_status text;
BEGIN
    -- Resolve model_id from provider + model key (best-effort)
    SELECT m.id INTO v_model_id
    FROM ai.models m
    JOIN ai.providers p ON p.id = m.provider_id
    WHERE p.key = p_provider AND m.key = p_model
    LIMIT 1;

    v_billing_status := CASE WHEN p_credit_cost > 0 THEN 'charged' ELSE 'free' END;

    -- Create request row
    INSERT INTO execution.requests (
        requester_lenser_id,
        origin_type,
        model_id,
        funding_source,
        runtime_origin
    ) VALUES (
        p_lenser_id,
        'api',
        v_model_id,
        CASE WHEN p_credit_cost > 0 THEN 'platform_credit' ELSE 'free' END,
        'cloud'
    )
    RETURNING id INTO v_request_id;

    -- Create run row with response
    INSERT INTO execution.runs (
        request_id,
        status,
        model_id,
        token_input,
        token_output,
        credit_cost,
        billing_status,
        response_text,
        response_meta,
        started_at,
        completed_at
    ) VALUES (
        v_request_id,
        p_status,
        v_model_id,
        p_token_input,
        p_token_output,
        p_credit_cost,
        v_billing_status,
        p_response_text,
        p_response_meta,
        now(),
        CASE WHEN p_status IN ('succeeded', 'failed', 'timed_out') THEN now() ELSE NULL END
    )
    RETURNING id INTO v_run_id;

    RETURN v_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) IS 'Persists an AI execution response by creating request + run rows. Called fire-and-forget by the API Worker after provider calls complete.';



CREATE OR REPLACE FUNCTION "public"."fn_expire_byok_keys"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'audit', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  UPDATE execution.byok_keys
  SET revoked_at = now()
  WHERE expires_at < now()
    AND revoked_at IS NULL;

  GET DIAGNOSTICS v_count = ROW_COUNT;

  IF v_count > 0 THEN
    INSERT INTO audit.events (event_type, actor_type, severity, payload)
    VALUES (
      'byok.keys_expired',
      'system',
      'info',
      jsonb_build_object('expired_count', v_count, 'expired_at', now())
    );
  END IF;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_expire_byok_keys"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_expire_byok_keys"() IS 'AR: Soft-revokes all BYOK keys where expires_at < now(). Called by pg_cron every hour. Returns number of keys revoked. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_expire_media_objects"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  UPDATE media.objects
  SET lifecycle_state = 'archived'
  WHERE expires_at < now()
    AND lifecycle_state NOT IN ('deleted', 'archived');

  GET DIAGNOSTICS v_count = ROW_COUNT;

  IF v_count > 0 THEN
    INSERT INTO audit.events (event_type, actor_type, severity, payload)
    VALUES (
      'media.objects_expired',
      'system',
      'info',
      jsonb_build_object('archived_count', v_count, 'archived_at', now())
    );
  END IF;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_expire_media_objects"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_expire_media_objects"() IS 'AT: Sets lifecycle_state=archived for objects past expires_at. Called by pg_cron daily at 02:00. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_expire_stale_approvals"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lenses', 'public'
    AS $$
DECLARE
  v_threshold_hours integer := COALESCE(
    NULLIF(current_setting('app.approval_timeout_hours', true), '')::integer,
    24
  );
  v_cutoff   timestamptz := now() - (v_threshold_hours * interval '1 hour');
  v_expired  integer     := 0;
  r          RECORD;
BEGIN
  FOR r IN
    SELECT id, workflow_run_id
    FROM   agents.team_runs
    WHERE  approval_status = 'pending'
      AND  created_at < v_cutoff
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      UPDATE agents.team_runs
      SET    approval_status = 'timed_out',
             status          = 'cancelled',
             completed_at    = COALESCE(completed_at, now()),
             metadata        = COALESCE(metadata, '{}'::jsonb)
                                 || jsonb_build_object(
                                      'timed_out_at', now(),
                                      'timeout_hours', v_threshold_hours
                                    ),
             updated_at      = now()
      WHERE  id = r.id;

      IF r.workflow_run_id IS NOT NULL THEN
        UPDATE lenses.workflow_runs
        SET    status        = 'timed_out',
               completed_at  = COALESCE(completed_at, now())
        WHERE  id = r.workflow_run_id
          AND  status NOT IN ('completed', 'cancelled', 'failed', 'timed_out');
      END IF;

      INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
      VALUES (
        r.id,
        'approval_timed_out',
        jsonb_build_object(
          'timeout_hours',     v_threshold_hours,
          'workflow_run_id',   r.workflow_run_id,
          'expired_at',        now()
        ),
        now()
      );

      v_expired := v_expired + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_expire_stale_approvals: team_run % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN v_expired;
END;
$$;


ALTER FUNCTION "public"."fn_expire_stale_approvals"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_expire_stale_approvals"() IS 'Phase K1: pg_cron target. Expires pending team_runs older than app.approval_timeout_hours (default 24h). Writes approval_status=timed_out, cancels the underlying workflow_run with status=timed_out, and emits an approval_timed_out event. Idempotent and concurrency-safe.';



CREATE OR REPLACE FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_bundle jsonb;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  SELECT jsonb_build_object(
    'ai_lenser',        to_jsonb(al),
    'settings',         to_jsonb(ws),
    'teams',            COALESCE((SELECT jsonb_agg(to_jsonb(t))  FROM agents.teams t  WHERE t.ai_lenser_id = al.id), '[]'::jsonb),
    'memory_profiles',  COALESCE((SELECT jsonb_agg(to_jsonb(m))  FROM agents.memory_profiles m WHERE m.ai_lenser_id = al.id), '[]'::jsonb),
    'personality',      COALESCE((SELECT jsonb_agg(to_jsonb(p))  FROM agents.personality_profiles p WHERE p.ai_lenser_id = al.id), '[]'::jsonb),
    'tool_profiles',    COALESCE((SELECT jsonb_agg(to_jsonb(tp)) FROM agents.tool_profiles tp WHERE tp.ai_lenser_id = al.id), '[]'::jsonb),
    'model_profiles',   COALESCE((SELECT jsonb_agg(to_jsonb(mp)) FROM agents.model_profiles mp WHERE mp.ai_lenser_id = al.id), '[]'::jsonb),
    'tool_assignments', COALESCE((SELECT jsonb_agg(to_jsonb(ta)) FROM agents.tool_assignments ta WHERE ta.ai_lenser_id = al.id), '[]'::jsonb),
    'exported_at',      now()
  )
  INTO v_bundle
  FROM agents.ai_lensers al
  LEFT JOIN agents.workspace_settings ws ON ws.ai_lenser_id = al.id
  WHERE al.id = p_ai_lenser_id;

  RETURN v_bundle;
END;
$$;


ALTER FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  INSERT INTO audit.vote_anomalies (battle_id, voter_lenser_id, anomaly_type, score)
  VALUES (p_battle_id, p_voter_id, p_type, p_score);
END;
$$;


ALTER FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) IS 'CA: Inserts a vote anomaly record. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) RETURNS integer
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_ack_commands(p_command_ids);
$$;


ALTER FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_approve_device(p_device_id);
$$;


ALTER FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS SETOF "agents"."gateway_commands"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT * FROM agents.fn_gateway_claim_commands(p_device_id, p_limit);
$$;


ALTER FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) IS 'Deprecated since Phase BG. Use public.fn_gateway_claim_commands_v2.';



CREATE OR REPLACE FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS TABLE("id" "uuid", "device_id" "uuid", "command_type" "text", "payload" "jsonb", "created_at" timestamp with time zone, "claimed_at" timestamp with time zone, "acked_at" timestamp with time zone, "envelope_sig" "text", "envelope_nonce" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT * FROM agents.fn_gateway_claim_commands_v2(p_device_id, p_limit);
$$;


ALTER FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text" DEFAULT NULL::"text", "p_daemon_version" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_heartbeat(p_device_id, p_public_key, p_hostname, p_daemon_version);
$$;


ALTER FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_revoke_device(p_device_id);
$$;


ALTER FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000001'::"uuid") RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "starts_at" timestamp with time zone, "ends_at" timestamp with time zone, "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
  SELECT id, slug, name, starts_at, ends_at, is_active
  FROM xp.seasons
  WHERE app_id = p_app_id
    AND is_active = true
    AND now() BETWEEN starts_at AND ends_at
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer DEFAULT 30, "p_model_key" "text" DEFAULT NULL::"text", "p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'agents', 'lenses'
    AS $$
DECLARE
  v_cost_by_model    jsonb;
  v_cost_time_series jsonb;
  v_eval_quality     jsonb;
  v_workflow_perf    jsonb;
  v_jwt_role text := coalesce(nullif(trim(both from current_setting('request.jwt.claim.role', true)), ''), '');
BEGIN
  -- Ownership guard: human owners pass agents.can_manage_ai_lenser.
  -- Service-role JWT (platform / trusted workers) may read any agent aggregates.
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    IF v_jwt_role IS DISTINCT FROM 'service_role' THEN
      RAISE EXCEPTION 'Insufficient privilege'
        USING ERRCODE = '42501';
    END IF;
  END IF;

  SELECT jsonb_agg(row_to_json(t)) INTO v_cost_by_model
  FROM (
    SELECT
      model_key,
      provider,
      SUM(total_credits)    AS total_credits,
      SUM(total_tokens_in)  AS total_tokens_in,
      SUM(total_tokens_out) AS total_tokens_out,
      SUM(run_count)        AS run_count
    FROM analytics.agent_cost_daily
    WHERE ai_lenser_id = p_ai_lenser_id
      AND period_date  >= CURRENT_DATE - p_days
      AND (p_model_key IS NULL OR model_key = p_model_key)
    GROUP BY model_key, provider
    ORDER BY SUM(total_credits) DESC
  ) t;

  SELECT jsonb_agg(row_to_json(t)) INTO v_cost_time_series
  FROM (
    SELECT
      period_date,
      SUM(total_credits) AS total_credits
    FROM analytics.agent_cost_daily
    WHERE ai_lenser_id = p_ai_lenser_id
      AND period_date  >= CURRENT_DATE - p_days
      AND (p_model_key IS NULL OR model_key = p_model_key)
    GROUP BY period_date
    ORDER BY period_date
  ) t;

  SELECT jsonb_agg(row_to_json(t)) INTO v_eval_quality
  FROM (
    SELECT
      eqd.period_date,
      e.name                                            AS evaluation_name,
      eqd.pass_count::float / NULLIF(eqd.run_count, 0) AS pass_rate,
      eqd.mean_score
    FROM analytics.eval_quality_daily eqd
    JOIN agents.evaluations e ON e.id = eqd.evaluation_id
    WHERE eqd.ai_lenser_id = p_ai_lenser_id
      AND eqd.period_date  >= CURRENT_DATE - p_days
    ORDER BY eqd.period_date
  ) t;

  SELECT jsonb_agg(row_to_json(t)) INTO v_workflow_perf
  FROM (
    SELECT
      wpd.period_date,
      wf.title                                              AS workflow_title,
      wpd.p50_duration_ms,
      wpd.p95_duration_ms,
      wpd.failed_count::float / NULLIF(wpd.run_count, 0)   AS failure_rate
    FROM analytics.workflow_perf_daily wpd
    JOIN lenses.workflows wf ON wf.id = wpd.workflow_id
    WHERE wpd.ai_lenser_id = p_ai_lenser_id
      AND wpd.period_date  >= CURRENT_DATE - p_days
      AND (p_workflow_id IS NULL OR wpd.workflow_id = p_workflow_id)
    ORDER BY wpd.period_date
  ) t;

  RETURN jsonb_build_object(
    'cost_by_model',    COALESCE(v_cost_by_model,    '[]'::jsonb),
    'cost_time_series', COALESCE(v_cost_time_series, '[]'::jsonb),
    'eval_quality',     COALESCE(v_eval_quality,     '[]'::jsonb),
    'workflow_perf',    COALESCE(v_workflow_perf,    '[]'::jsonb)
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") IS 'Owner-only analytics summary for an AI lenser (human owners via agents.can_manage_ai_lenser). Callers with request.jwt.claim.role = service_role may read aggregates for any agent (platform / trusted workers). Returns cost_by_model, cost_time_series, eval_quality, and workflow_perf aggregated over the last p_days days. Optional p_model_key and p_workflow_id narrow cost and workflow results. Reads only from analytics.* pre-aggregated tables; never touches raw logs.';



CREATE OR REPLACE FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 100, "p_offset" integer DEFAULT 0) RETURNS TABLE("kind" "text", "id" "text", "occurred_at" timestamp with time zone, "title" "text", "result" "text", "workflow_id" "uuid", "workflow_title" "text", "run_id" "uuid", "schedule_id" "uuid", "action_type" "text", "event_type" "text", "payload" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lenses', 'lensers'
    AS $$
  WITH authorized_agent AS (
    SELECT al.id AS ai_lenser_id, al.profile_id
    FROM agents.ai_lensers al
    JOIN agents.ownerships o ON o.ai_lenser_id = al.id
    WHERE al.id = p_ai_lenser_id
      AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND o.revoked_at IS NULL
    LIMIT 1
  ),
  action_items AS (
    SELECT
      CASE
        WHEN l.action_type IN ('dispatch_schedule', 'schedule_skipped') THEN 'schedule_dispatch'
        ELSE 'agent_action'
      END AS kind,
      l.id::text AS id,
      l.occurred_at,
      COALESCE(l.action_type, 'agent_action') AS title,
      l.result,
      CASE WHEN l.metadata ? 'workflow_id' THEN (l.metadata ->> 'workflow_id')::uuid END AS workflow_id,
      NULL::text AS workflow_title,
      CASE WHEN l.metadata ? 'run_id' THEN (l.metadata ->> 'run_id')::uuid END AS run_id,
      CASE WHEN l.metadata ? 'schedule_id' THEN (l.metadata ->> 'schedule_id')::uuid END AS schedule_id,
      l.action_type,
      NULL::text AS event_type,
      l.metadata AS payload
    FROM agents.action_logs l
    JOIN authorized_agent aa ON aa.ai_lenser_id = l.ai_lenser_id
  ),
  run_items AS (
    SELECT
      'workflow_run'::text AS kind,
      r.id::text AS id,
      COALESCE(r.completed_at, r.started_at, r.created_at) AS occurred_at,
      r.status AS title,
      r.status AS result,
      w.id AS workflow_id,
      w.title AS workflow_title,
      r.id AS run_id,
      r.schedule_id,
      NULL::text AS action_type,
      NULL::text AS event_type,
      jsonb_build_object(
        'status', r.status,
        'trigger_mode', r.trigger_mode,
        'spent_credits', r.spent_credits
      ) AS payload
    FROM authorized_agent aa
    JOIN lenses.workflows w ON w.lenser_id = aa.profile_id
    JOIN lenses.workflow_runs r ON r.workflow_id = w.id
  ),
  event_items AS (
    SELECT
      'workflow_event'::text AS kind,
      concat(e.run_id::text, ':', e.event_id::text) AS id,
      e.created_at AS occurred_at,
      e.type AS title,
      NULL::text AS result,
      w.id AS workflow_id,
      w.title AS workflow_title,
      e.run_id AS run_id,
      r.schedule_id,
      NULL::text AS action_type,
      e.type AS event_type,
      COALESCE(e.payload, '{}'::jsonb) AS payload
    FROM authorized_agent aa
    JOIN lenses.workflows w ON w.lenser_id = aa.profile_id
    JOIN lenses.workflow_runs r ON r.workflow_id = w.id
    JOIN lenses.workflow_run_events e ON e.run_id = r.id
  )
  SELECT *
  FROM (
    SELECT * FROM action_items
    UNION ALL
    SELECT * FROM run_items
    UNION ALL
    SELECT * FROM event_items
  ) feed
  ORDER BY occurred_at DESC, id DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 250)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Owner-only unified automation feed for AI workspace panels. Unions agents.action_logs with workflow_runs and workflow_run_events for workflows owned by the AI lenser profile.';



CREATE OR REPLACE FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") RETURNS TABLE("period_date" "date", "credits_spent" integer, "battles_used" integer, "votes_used" integer, "spending_limit" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT qs.period_date, qs.credits_spent, qs.battles_used, qs.votes_used,
         pol.spending_limit_credits
  FROM agents.quota_snapshots qs
  LEFT JOIN agents.policies pol ON pol.ai_lenser_id = qs.ai_lenser_id
  WHERE qs.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY qs.period_date DESC
  LIMIT 30;
$$;


ALTER FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(v.*)
  FROM agents.v_agent_profile v
  WHERE v.id = p_ai_lenser_id
    AND (
      v.owner_lenser_id = lensers.get_auth_human_lenser_id()
      OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(v.*)
  FROM agents.v_agent_profile v
  WHERE v.profile_id = p_profile_id
    AND (v.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date" DEFAULT CURRENT_DATE) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "period_date" "date", "battles_used" integer, "votes_used" integer, "credits_spent" integer, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT qs.id, qs.ai_lenser_id, qs.period_date, qs.battles_used,
         qs.votes_used, qs.credits_spent, qs.updated_at
  FROM agents.quota_snapshots qs
  WHERE qs.ai_lenser_id = p_ai_lenser_id
    AND qs.period_date  = p_period_date
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_profile_id uuid;
  v_ai_lenser_id uuid;
  v_owner_ok boolean;
BEGIN
  SELECT p.id, al.id
  INTO v_profile_id, v_ai_lenser_id
  FROM lensers.profiles p
  JOIN agents.ai_lensers al ON al.profile_id = p.id
  WHERE p.handle = p_profile_handle
    AND p.type = 'ai'
  LIMIT 1;

  IF v_ai_lenser_id IS NULL THEN
    RETURN NULL;
  END IF;

  SELECT agents.can_manage_ai_lenser(v_ai_lenser_id) INTO v_owner_ok;
  IF NOT v_owner_ok THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI workspace'
      USING ERRCODE = '42501';
  END IF;

  RETURN jsonb_build_object(
    'profile_id', v_profile_id,
    'ai_lenser_id', v_ai_lenser_id,
    'teams', COALESCE((
      SELECT jsonb_agg(
        jsonb_build_object(
          'id', t.id,
          'ai_lenser_id', t.ai_lenser_id,
          'name', t.name,
          'description', t.description,
          'status', t.status,
          'is_active', t.is_active,
          'scratchpad', t.scratchpad,
          'member_count', (
            SELECT count(*) FROM agents.team_members tm WHERE tm.team_id = t.id AND tm.is_active = true
          ),
          'members', COALESCE((
            SELECT jsonb_agg(tm ORDER BY tm.lane, tm.sort_order, tm.created_at)
            FROM agents.team_members tm
            WHERE tm.team_id = t.id
          ), '[]'::jsonb),
          'edges', COALESCE((
            SELECT jsonb_agg(te ORDER BY te.created_at)
            FROM agents.team_edges te
            WHERE te.team_id = t.id
          ), '[]'::jsonb),
          'created_at', t.created_at,
          'updated_at', t.updated_at
        )
        ORDER BY t.updated_at DESC
      )
      FROM agents.teams t
      WHERE t.ai_lenser_id = v_ai_lenser_id
    ), '[]'::jsonb),
    'runs', COALESCE((
      SELECT jsonb_agg(
        jsonb_build_object(
          'id', tr.id,
          'team_id', tr.team_id,
          'workflow_id', tr.workflow_id,
          'workflow_run_id', tr.workflow_run_id,
          'status', tr.status,
          'approval_status', tr.approval_status,
          'started_at', tr.started_at,
          'completed_at', tr.completed_at,
          'created_at', tr.created_at,
          'metadata', tr.metadata
        )
        ORDER BY tr.created_at DESC
      )
      FROM agents.team_runs tr
      WHERE tr.ai_lenser_id = v_ai_lenser_id
      LIMIT 20
    ), '[]'::jsonb),
    'profiles', jsonb_build_object(
      'personality', COALESCE((SELECT jsonb_agg(pp ORDER BY pp.updated_at DESC) FROM agents.personality_profiles pp WHERE pp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb),
      'memory', COALESCE((SELECT jsonb_agg(mp ORDER BY mp.updated_at DESC) FROM agents.memory_profiles mp WHERE mp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb),
      'tools', COALESCE((SELECT jsonb_agg(tp ORDER BY tp.updated_at DESC) FROM agents.tool_profiles tp WHERE tp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb),
      'models', COALESCE((SELECT jsonb_agg(modp ORDER BY modp.updated_at DESC) FROM agents.model_profiles modp WHERE modp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb)
    ),
    'workflow_assignments', COALESCE((
      SELECT jsonb_agg(wa ORDER BY wa.updated_at DESC)
      FROM agents.workflow_assignments wa
      WHERE wa.ai_lenser_id = v_ai_lenser_id
    ), '[]'::jsonb)
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "max_tokens_per_second" integer, "injected_delay_ms" integer, "max_context_tokens" integer, "allowed_model_tier" "text", "time_budget_ms" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT hp.id, hp.battle_id, hp.max_tokens_per_second, hp.injected_delay_ms,
         hp.max_context_tokens, hp.allowed_model_tier, hp.time_budget_ms
  FROM battles.ai_handicap_policies hp
  WHERE hp.battle_id = p_battle_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "criterion_id" "uuid", "score" numeric, "rationale" "text", "model_key" "text", "run_id" "uuid", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT id, contender_id, criterion_id, score, rationale, model_key, run_id, created_at
  FROM battles.fn_get_ai_judge_verdicts(p_battle_id);
$$;


ALTER FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") RETURNS TABLE("id" "uuid", "key" "text", "name" "text", "provider_id" "uuid", "description" "text", "capabilities" "text"[], "temperature" numeric, "max_tokens" integer, "context_window_tokens" integer, "supports_tools" boolean, "supports_vision" boolean, "is_active" boolean, "input_modalities" "text"[], "output_modalities" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT
    m.id, m.key, m.name, m.provider_id, m.description,
    m.capabilities, m.temperature, m.max_tokens, m.context_window_tokens,
    m.supports_tools, m.supports_vision,
    m.is_active, m.input_modalities, m.output_modalities,
    m.created_at
  FROM ai.models m
  WHERE m.id = p_model_id;
$$;


ALTER FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") IS 'Security wrapper: fetch an AI model row by ID.';



CREATE OR REPLACE FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") RETURNS TABLE("id" "uuid", "key" "text", "display_name" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT p.id, p.key, p.display_name
  FROM ai.providers p
  WHERE p.id = p_provider_id;
$$;


ALTER FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") IS 'Security wrapper: fetch AI provider id, key, and display_name by provider UUID.';



CREATE OR REPLACE FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ar.*)
  FROM agents.approval_requests_v ar
  WHERE ar.request_id = p_request_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = ar.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_auth_profile_gate"() RETURNS TABLE("status" "text", "deletion_requested_at" timestamp with time zone, "deletion_deadline_at" timestamp with time zone, "onboarding_step" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT p.status, p.deletion_requested_at, p.deletion_deadline_at, p.onboarding_step
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_auth_profile_gate"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_auth_profile_gate"() IS 'Security wrapper: return status/deletion/onboarding gate fields for the current user''s profile.';



CREATE OR REPLACE FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) RETURNS TABLE("entity_id" "uuid", "lenser_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT r.entity_id, r.lenser_id, r.reaction, r.created_at
  FROM content.reactions r
  WHERE r.entity_type::text = p_entity_type
    AND r.entity_id   = ANY(p_entity_ids);
$$;


ALTER FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) IS 'Security wrapper: return all reaction rows for an array of entity IDs (same type).';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_slug" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "finalized_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone, "task_source" "text", "contender_structure" "text", "judging_mode" "text", "challenge_type" "text", "shared_input_snapshot" "jsonb", "lenser_battle_policy" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT
    b.id, b.slug, b.title, b.task_prompt, b.status, b.total_vote_count,
    b.published_at, b.voting_opens_at, b.voting_closes_at, b.finalized_at,
    b.battle_type::text, b.voter_eligibility::text, b.handicap_config,
    b.creator_lenser_id, b.forum_thread_id,
    b.workflow_id, b.lens_id, b.execution_starts_at,
    b.auto_publish, b.voting_duration_hours, b.vote_velocity, b.og_image_url,
    b.winner_contender_id, b.parent_battle_id, b.deleted_at,
    -- V2 axes
    b.task_source,
    b.contender_structure::text,
    b.judging_mode::text,
    b.challenge_type,
    b.shared_input_snapshot,
    b.lenser_battle_policy
  FROM battles.battles b
  WHERE (p_battle_id IS NULL OR b.id   = p_battle_id)
    AND (p_slug      IS NULL OR b.slug = p_slug)
    AND b.deleted_at IS NULL
    AND (
      b.status <> 'draft'
      OR b.creator_lenser_id = lensers.get_auth_lenser_id()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") IS 'Security wrapper: look up a battle by id or slug. Public battles are visible to everyone; drafts only to the creator. Returns NULL when not found or draft-and-not-creator. Now includes V2 fields: task_source, contender_structure, judging_mode, challenge_type, shared_input_snapshot, lenser_battle_policy.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "finalized_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone, "task_source" "text", "contender_structure" "text", "judging_mode" "text", "challenge_type" "text", "shared_input_snapshot" "jsonb", "lenser_battle_policy" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT * FROM public.fn_get_battle(NULL::"uuid", p_slug);
$$;


ALTER FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") IS 'Alias for fn_get_battle(p_slug). Return type kept in sync with fn_get_battle V2 (Phase CS V2 fields: task_source, contender_structure, judging_mode, challenge_type, shared_input_snapshot, lenser_battle_policy).';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "battle_id" "uuid", "lenser_id" "uuid", "body" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "lenser_handle" "text", "lenser_display_name" "text", "lenser_avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
  SELECT
    c.id,
    c.battle_id,
    c.lenser_id,
    c.body,
    c.created_at,
    c.updated_at,
    p.handle          AS lenser_handle,
    p.display_name    AS lenser_display_name,
    p.avatar_url      AS lenser_avatar_url
  FROM battles.comments  c
  JOIN lensers.profiles  p ON p.id = c.lenser_id
  WHERE c.battle_id = p_battle_id
  ORDER BY c.created_at ASC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer DEFAULT 50, "p_before_ts" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_before_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "lenser_id" "uuid", "body" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "lenser_handle" "text", "lenser_display_name" "text", "lenser_avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
  SELECT
    c.id,
    c.battle_id,
    c.lenser_id,
    c.body,
    c.created_at,
    c.updated_at,
    p.handle          AS lenser_handle,
    p.display_name    AS lenser_display_name,
    p.avatar_url      AS lenser_avatar_url
  FROM battles.comments c
  JOIN lensers.profiles p ON p.id = c.lenser_id
  WHERE c.battle_id = p_battle_id
    AND (
      p_before_ts IS NULL
      OR (c.created_at, c.id) < (p_before_ts, p_before_id)
    )
  ORDER BY c.created_at DESC, c.id DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "contender_type" "text", "display_name" "text", "contender_ref_id" "uuid", "contender_status" "text", "entry_mode" "text", "joined_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT c.id, c.battle_id, c.slot::text, c.contender_type::text, c.display_name,
         c.contender_ref_id, c.contender_status, c.entry_mode,
         c.joined_at
  FROM battles.contenders c
  WHERE c.battle_id = p_battle_id
  ORDER BY c.slot;
$$;


ALTER FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") RETURNS TABLE("battle_id" "uuid", "winner_lenser_id" "uuid", "loser_lenser_id" "uuid", "winner_score_before" numeric, "winner_score_after" numeric, "loser_score_before" numeric, "loser_score_after" numeric, "is_draw" boolean, "k_factor" integer, "computed_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT
    el.battle_id, el.winner_lenser_id, el.loser_lenser_id,
    el.winner_score_before, el.winner_score_after,
    el.loser_score_before,  el.loser_score_after,
    el.is_draw, el.k_factor, el.computed_at
  FROM reputation.elo_battle_log el
  WHERE el.battle_id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") IS 'Security wrapper: return ELO score change records for a specific battle.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT to_jsonb(ec.*)
  FROM battles.execution_configs ec
  WHERE ec.battle_id = p_battle_id
    AND EXISTS (
      SELECT 1 FROM battles.battles b
      WHERE b.id = ec.battle_id
        AND (b.creator_lenser_id = lensers.get_auth_lenser_id()
             OR b.status::text IN ('published', 'closed'))
    );
$$;


ALTER FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "status" "text", "worker_id" "text", "claimed_at" timestamp with time zone, "completed_at" timestamp with time zone, "retry_count" integer, "max_retries" integer, "error_message" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT j.id, j.battle_id, j.contender_id, j.slot, j.status, j.worker_id,
         j.claimed_at, j.completed_at, j.retry_count, j.max_retries,
         j.error_message, j.created_at
  FROM battles.battle_execution_jobs j
  JOIN battles.battles b ON b.id = j.battle_id
  WHERE j.battle_id = p_battle_id
    AND (b.creator_lenser_id = lensers.get_auth_lenser_id() OR public.is_admin())
  ORDER BY j.slot;
$$;


ALTER FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."battles" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "creator_lenser_id" "uuid" DEFAULT "lensers"."get_auth_lenser_id"() NOT NULL,
    "title" "text" NOT NULL,
    "slug" "text" NOT NULL,
    "task_prompt" "text" NOT NULL,
    "rubric_id" "uuid",
    "status" "battles"."battle_status_enum" DEFAULT 'draft'::"battles"."battle_status_enum" NOT NULL,
    "invite_code" "text",
    "forum_thread_id" "uuid",
    "max_contenders" integer DEFAULT 2 NOT NULL,
    "voting_opens_at" timestamp with time zone,
    "voting_closes_at" timestamp with time zone,
    "published_at" timestamp with time zone,
    "finalized_at" timestamp with time zone,
    "winner_contender_id" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "deleted_at" timestamp with time zone,
    "total_vote_count" integer DEFAULT 0 NOT NULL,
    "battle_type" "battles"."battle_type_enum" DEFAULT 'human_vs_human_open_votes'::"battles"."battle_type_enum" NOT NULL,
    "voter_eligibility" "battles"."voter_eligibility_enum" DEFAULT 'open'::"battles"."voter_eligibility_enum" NOT NULL,
    "handicap_config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "workflow_id" "uuid",
    "lens_id" "uuid",
    "content_type" "text" DEFAULT 'text'::"text" NOT NULL,
    "execution_starts_at" timestamp with time zone,
    "executing_started_at" timestamp with time zone,
    "auto_publish" boolean DEFAULT true NOT NULL,
    "voting_duration_hours" integer DEFAULT 24 NOT NULL,
    "vote_velocity" numeric(8,4) DEFAULT 0 NOT NULL,
    "og_image_url" "text",
    "ai_judge_enabled" boolean DEFAULT false,
    "ai_judge_model_key" "text" DEFAULT 'claude-sonnet-4-6'::"text",
    "ai_judge_prompt" "text",
    "parent_battle_id" "uuid",
    "template_id" "uuid",
    "auto_assign_contenders" boolean DEFAULT false NOT NULL,
    "auto_promote" boolean DEFAULT false NOT NULL,
    "shared_input_snapshot" "jsonb",
    "lenser_battle_policy" "jsonb",
    "contender_structure" "battles"."contender_structure_enum",
    "judging_mode" "battles"."judging_mode_enum",
    "task_source" "text",
    "challenge_type" "text",
    "generated_challenge_id" "uuid",
    "archived_at" timestamp with time zone,
    "benchmark_config" "jsonb",
    "normalized_score" numeric(5,4) DEFAULT NULL::numeric,
    CONSTRAINT "battles_content_type_check" CHECK (("content_type" = ANY (ARRAY['text'::"text", 'code'::"text", 'poem'::"text", 'drawing'::"text", 'image'::"text", 'video'::"text", 'audio'::"text", 'workflow'::"text", 'map'::"text", 'avatar'::"text", 'image_edit'::"text", 'kaggle'::"text"]))),
    CONSTRAINT "battles_max_contenders_min" CHECK (("max_contenders" >= 2)),
    CONSTRAINT "battles_normalized_score_range" CHECK ((("normalized_score" IS NULL) OR (("normalized_score" >= (0)::numeric) AND ("normalized_score" <= (1)::numeric)))),
    CONSTRAINT "battles_voting_window_order" CHECK ((("voting_opens_at" IS NULL) OR ("voting_closes_at" IS NULL) OR ("voting_opens_at" < "voting_closes_at"))),
    CONSTRAINT "chk_challenge_type_values" CHECK ((("challenge_type" IS NULL) OR ("challenge_type" = ANY (ARRAY['writing_contest'::"text", 'math_calculation'::"text", 'grammar_quiz'::"text", 'hand_drawing'::"text", 'fill_in_blanks'::"text", 'first_code_error'::"text", 'logic_puzzle'::"text", 'prompt_duel'::"text", 'debate'::"text", 'code_completion_benchmark'::"text", 'instruction_following_benchmark'::"text", 'reasoning_benchmark'::"text"])))),
    CONSTRAINT "chk_lenser_battle_policy_format" CHECK ((("lenser_battle_policy" IS NULL) OR ((("lenser_battle_policy" ->> 'memory_mode'::"text") = ANY (ARRAY['clean_room'::"text", 'personality'::"text", 'unrestricted'::"text"])) AND (("lenser_battle_policy" ->> 'instruction_disclosure'::"text") = ANY (ARRAY['hidden'::"text", 'visible_after_close'::"text", 'always_visible'::"text"]))))),
    CONSTRAINT "chk_task_source_battle_type_compat" CHECK ((("task_source" IS NULL) OR ("task_source" = 'challenge'::"text") OR (("task_source" = ANY (ARRAY['lens'::"text", 'workflow'::"text"])) AND ("battle_type" = ANY (ARRAY['ai_vs_ai'::"battles"."battle_type_enum", 'workflow_battle'::"battles"."battle_type_enum", 'lenser_battle'::"battles"."battle_type_enum"]))))),
    CONSTRAINT "chk_task_source_values" CHECK ((("task_source" IS NULL) OR ("task_source" = ANY (ARRAY['lens'::"text", 'workflow'::"text", 'challenge'::"text"])))),
    CONSTRAINT "ck_battles_battles_ai_judge_prompt_len" CHECK ((("ai_judge_prompt" IS NULL) OR ("length"("ai_judge_prompt") <= 8000))),
    CONSTRAINT "ck_battles_battles_invite_code_len" CHECK ((("invite_code" IS NULL) OR ("length"("invite_code") <= 32))),
    CONSTRAINT "ck_battles_battles_task_prompt_len" CHECK ((("task_prompt" IS NULL) OR ("length"("task_prompt") <= 32000))),
    CONSTRAINT "ck_battles_battles_title_len" CHECK ((("title" IS NULL) OR ("length"("title") <= 200))),
    CONSTRAINT "ck_battles_battles_total_vote_count_nonneg" CHECK ((("total_vote_count" IS NULL) OR ("total_vote_count" >= 0)))
);


ALTER TABLE "battles"."battles" OWNER TO "postgres";


COMMENT ON COLUMN "battles"."battles"."battle_type" IS 'Determines who the contenders are: ai_vs_ai, human_vs_human_ai_votes, human_vs_human_open_votes, human_vs_ai.';



COMMENT ON COLUMN "battles"."battles"."voter_eligibility" IS 'Who is allowed to cast votes: open (everyone), human_only, ai_only, verified_lenser.';



COMMENT ON COLUMN "battles"."battles"."handicap_config" IS 'JSONB snapshot of the active ai_handicap_policies row for read-only audit. Authoritative config lives in battles.ai_handicap_policies.';



COMMENT ON COLUMN "battles"."battles"."parent_battle_id" IS 'Phase V1: when this battle was created as a rematch of another battle, this points to the parent. NULL for organic battles. ON DELETE SET NULL preserves rematch history when an upstream parent is hard-deleted.';



COMMENT ON COLUMN "battles"."battles"."template_id" IS 'Phase BF: NULL for ad-hoc battles, populated when created via fn_battles_create_from_template. Used by trg_notify_template_battle_status.';



COMMENT ON COLUMN "battles"."battles"."auto_assign_contenders" IS 'CB: When true, fn_battles_auto_schedule_contenders assigns AI lensers up to max_contenders.';



COMMENT ON COLUMN "battles"."battles"."auto_promote" IS 'CB: When true, fn_battles_auto_promote transitions draft→open when readiness check passes.';



COMMENT ON COLUMN "battles"."battles"."shared_input_snapshot" IS 'Immutable snapshot of shared Lens [[parameter]] values resolved at battle creation time. All contenders receive these identical inputs.';



COMMENT ON COLUMN "battles"."battles"."lenser_battle_policy" IS 'Memory mode, instruction disclosure, and model binding override rules for Lenser Battles. NULL for non-Lenser Battle formats.';



COMMENT ON COLUMN "battles"."battles"."contender_structure" IS 'V2 axis: who competes — ai_vs_ai, human_vs_ai, or human_vs_human. For lens/workflow task sources this is always ai_vs_ai (enforced by chk_task_source_battle_type_compat).';



COMMENT ON COLUMN "battles"."battles"."judging_mode" IS 'V2 axis: how the winner is decided — community_vote, ai_judge, rubric_score, or auto_score.';



COMMENT ON COLUMN "battles"."battles"."task_source" IS 'V2 axis: the execution contract that defines the battle — lens, workflow, or challenge. Lens and workflow task sources are AI-only; human contenders are only valid for challenge.';



COMMENT ON COLUMN "battles"."battles"."challenge_type" IS 'V2: challenge game type ID for human contests (e.g. writing_contest, math_calculation).';



COMMENT ON COLUMN "battles"."battles"."generated_challenge_id" IS 'V2: reference to the locked AI-generated challenge for human contest battles.';



COMMENT ON COLUMN "battles"."battles"."benchmark_config" IS 'Benchmark game metadata. NULL = non-benchmark battle. Shape: { game_key, export_eligible, anonymization_policy, required_capabilities[], dataset_schema_version }.';



COMMENT ON COLUMN "battles"."battles"."normalized_score" IS 'Normalized winner score 0.0–1.0 for benchmark dataset comparison. NULL for non-benchmark battles. Populated after finalization.';



CREATE TABLE IF NOT EXISTS "battles"."contender_entity_map" (
    "contender_id" "uuid" NOT NULL,
    "profile_id" "uuid",
    "ai_lenser_id" "uuid",
    "group_id" "uuid",
    "redacted_agent_snapshot" "jsonb",
    "redacted_agent_snapshot_hash" "text",
    CONSTRAINT "exactly_one_entity_ref" CHECK (((((("profile_id" IS NOT NULL))::integer + (("ai_lenser_id" IS NOT NULL))::integer) + (("group_id" IS NOT NULL))::integer) = 1))
);


ALTER TABLE "battles"."contender_entity_map" OWNER TO "postgres";


COMMENT ON TABLE "battles"."contender_entity_map" IS 'CANONICAL typed identity resolution for battle contenders. battles.contenders.contender_ref_id is a deprecated compatibility alias. CHECK constraint enforces exactly one FK: profile, ai_lenser, or group. Auto-populated by trg_populate_contender_entity_map on contenders INSERT.';



CREATE TABLE IF NOT EXISTS "battles"."contenders" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "slot" character(1) NOT NULL,
    "contender_type" "battles"."contender_type_enum" NOT NULL,
    "contender_ref_id" "uuid" NOT NULL,
    "display_name" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "team_id" "uuid",
    "entry_mode" "text" DEFAULT 'direct'::"text" NOT NULL,
    "contender_status" "text" DEFAULT 'active'::"text" NOT NULL,
    "joined_at" timestamp with time zone DEFAULT "now"(),
    "accepted_at" timestamp with time zone,
    "withdrawn_at" timestamp with time zone,
    CONSTRAINT "contenders_entry_mode_check" CHECK (("entry_mode" = ANY (ARRAY['direct'::"text", 'invited'::"text", 'qualified'::"text", 'wildcard'::"text", 'auto_join'::"text"]))),
    CONSTRAINT "contenders_slot_letter" CHECK ((("slot" >= 'A'::"bpchar") AND ("slot" <= 'Z'::"bpchar"))),
    CONSTRAINT "contenders_status_check" CHECK (("contender_status" = ANY (ARRAY['pending'::"text", 'accepted'::"text", 'active'::"text", 'withdrawn'::"text", 'disqualified'::"text", 'eliminated'::"text"])))
);


ALTER TABLE "battles"."contenders" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."rule_snapshots" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "snapshot_hash" "text" NOT NULL,
    "rules_json" "jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "created_by" "uuid"
);


ALTER TABLE "battles"."rule_snapshots" OWNER TO "postgres";


COMMENT ON TABLE "battles"."rule_snapshots" IS 'Append-only snapshot of battle rules at publication. Frozen by public.fn_publish_battle. snapshot_hash = SHA-256(rules_json::text). One row per publication event (re-publication creates a new row).';



COMMENT ON COLUMN "battles"."rule_snapshots"."rules_json" IS 'Frozen JSON: {title, task_prompt, battle_type, voter_eligibility, max_contenders, rubric_id, handicap_config, published_at}. Does not include mutable fields (vote counts, winner, etc.).';



CREATE TABLE IF NOT EXISTS "battles"."vote_aggregates" (
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "raw_vote_count" integer DEFAULT 0 NOT NULL,
    "weighted_vote_sum" numeric DEFAULT 0 NOT NULL,
    "draw_count" integer DEFAULT 0 NOT NULL,
    "rank_position" integer,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_battles_vote_aggregates_draw_count_nonneg" CHECK ((("draw_count" IS NULL) OR ("draw_count" >= 0))),
    CONSTRAINT "ck_battles_vote_aggregates_raw_vote_count_nonneg" CHECK ((("raw_vote_count" IS NULL) OR ("raw_vote_count" >= 0))),
    CONSTRAINT "ck_battles_vote_aggregates_weighted_vote_sum_nonneg" CHECK ((("weighted_vote_sum" IS NULL) OR ("weighted_vote_sum" >= (0)::numeric)))
);


ALTER TABLE "battles"."vote_aggregates" OWNER TO "postgres";


CREATE OR REPLACE VIEW "battles"."v_battle_full" AS
 SELECT "b"."id",
    "b"."slug",
    "b"."title",
    "b"."task_prompt",
    "b"."status",
    "b"."battle_type",
    "b"."voter_eligibility",
    "b"."handicap_config",
    "b"."max_contenders",
    "b"."rubric_id",
    "b"."total_vote_count",
    "b"."published_at",
    "b"."voting_opens_at",
    "b"."voting_closes_at",
    "b"."finalized_at",
    "b"."winner_contender_id",
    "b"."creator_lenser_id",
    "b"."created_at",
    "b"."updated_at",
    "ca"."id" AS "contender_a_id",
    "ca"."display_name" AS "contender_a_name",
    "ca"."contender_type" AS "contender_a_type",
    "ca"."contender_status" AS "contender_a_status",
    "ema"."profile_id" AS "contender_a_profile_id",
    "ema"."ai_lenser_id" AS "contender_a_ai_lenser_id",
    "sa"."id" AS "submission_a_id",
    "sa"."content_text" AS "submission_a_text",
    "sa"."content_url" AS "submission_a_url",
    "sa"."status" AS "submission_a_status",
    "va"."raw_vote_count" AS "votes_a",
    "va"."weighted_vote_sum" AS "score_a",
    "va"."draw_count" AS "draw_count_a",
    "cb"."id" AS "contender_b_id",
    "cb"."display_name" AS "contender_b_name",
    "cb"."contender_type" AS "contender_b_type",
    "cb"."contender_status" AS "contender_b_status",
    "emb"."profile_id" AS "contender_b_profile_id",
    "emb"."ai_lenser_id" AS "contender_b_ai_lenser_id",
    "sb"."id" AS "submission_b_id",
    "sb"."content_text" AS "submission_b_text",
    "sb"."content_url" AS "submission_b_url",
    "sb"."status" AS "submission_b_status",
    "vb"."raw_vote_count" AS "votes_b",
    "vb"."weighted_vote_sum" AS "score_b",
        CASE
            WHEN ("b"."winner_contender_id" = "ca"."id") THEN 'A'::"text"
            WHEN ("b"."winner_contender_id" = "cb"."id") THEN 'B'::"text"
            ELSE NULL::"text"
        END AS "winner_slot",
    "rs"."snapshot_hash" AS "rule_snapshot_hash"
   FROM ((((((((("battles"."battles" "b"
     LEFT JOIN "battles"."contenders" "ca" ON ((("ca"."battle_id" = "b"."id") AND ("ca"."slot" = 'A'::"bpchar"))))
     LEFT JOIN "battles"."contender_entity_map" "ema" ON (("ema"."contender_id" = "ca"."id")))
     LEFT JOIN "battles"."submissions" "sa" ON ((("sa"."contender_id" = "ca"."id") AND ("sa"."is_final" = true) AND ("sa"."status" = 'submitted'::"battles"."submission_status_enum"))))
     LEFT JOIN "battles"."vote_aggregates" "va" ON ((("va"."battle_id" = "b"."id") AND ("va"."contender_id" = "ca"."id"))))
     LEFT JOIN "battles"."contenders" "cb" ON ((("cb"."battle_id" = "b"."id") AND ("cb"."slot" = 'B'::"bpchar"))))
     LEFT JOIN "battles"."contender_entity_map" "emb" ON (("emb"."contender_id" = "cb"."id")))
     LEFT JOIN "battles"."submissions" "sb" ON ((("sb"."contender_id" = "cb"."id") AND ("sb"."is_final" = true) AND ("sb"."status" = 'submitted'::"battles"."submission_status_enum"))))
     LEFT JOIN "battles"."vote_aggregates" "vb" ON ((("vb"."battle_id" = "b"."id") AND ("vb"."contender_id" = "cb"."id"))))
     LEFT JOIN LATERAL ( SELECT "rule_snapshots"."snapshot_hash"
           FROM "battles"."rule_snapshots"
          WHERE ("rule_snapshots"."battle_id" = "b"."id")
          ORDER BY "rule_snapshots"."created_at" DESC
         LIMIT 1) "rs" ON (true))
  WHERE ("b"."deleted_at" IS NULL);


ALTER VIEW "battles"."v_battle_full" OWNER TO "postgres";


COMMENT ON VIEW "battles"."v_battle_full" IS 'Full battle lobby state for BattleDetailPage. Used by fn_get_battle_full. Covers contenders, entity map, final submissions, vote aggregates, and rule snapshot hash in a single join. contender_entity_map is the canonical identity source for slot resolution.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_full"("p_slug" "text") RETURNS "battles"."v_battle_full"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT *
  FROM battles.v_battle_full
  WHERE slug = p_slug
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_battle_full"("p_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") IS 'Returns full battle state for BattleDetailPage by slug. Single roundtrip covers contenders, submissions, vote aggregates, entity map, and rule snapshot hash via v_battle_full view.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "status" "text", "claimed_at" timestamp with time zone, "completed_at" timestamp with time zone, "retry_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT id, battle_id, slot, status, claimed_at, completed_at, retry_count, created_at
  FROM battles.v_execution_jobs_public
  WHERE battle_id = p_battle_id
  ORDER BY slot;
$$;


ALTER FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_battle        battles.battles;
  v_battle_id     UUID;
  v_contenders    JSONB;
  v_submissions   JSONB;
  v_aggregates    JSONB;
  v_scorecards    JSONB;
  v_criteria      JSONB;
  v_verdicts      JSONB;
BEGIN
  -- Fetch battle and enforce status guard
  SELECT * INTO v_battle
  FROM battles.battles
  WHERE slug = p_battle_slug
    AND status IN ('published', 'closed');

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  v_battle_id := v_battle.id;

  SELECT COALESCE(jsonb_agg(row_to_json(c)), '[]')
  INTO v_contenders
  FROM (
    SELECT id, battle_id, slot, contender_type, display_name, lenser_id,
           contender_ref_id, status, invited_at, joined_at
    FROM battles.contenders
    WHERE battle_id = v_battle_id
    ORDER BY slot
  ) c;

  SELECT COALESCE(jsonb_agg(row_to_json(s)), '[]')
  INTO v_submissions
  FROM (
    SELECT id, battle_id, contender_id, content_text, content_url,
           submitted_at, word_count, char_count
    FROM battles.submissions
    WHERE battle_id = v_battle_id
  ) s;

  SELECT COALESCE(jsonb_agg(row_to_json(a)), '[]')
  INTO v_aggregates
  FROM (
    SELECT battle_id, contender_id, raw_vote_count, weighted_vote_sum,
           draw_count, rank_position
    FROM battles.vote_aggregates
    WHERE battle_id = v_battle_id
  ) a;

  SELECT COALESCE(jsonb_agg(row_to_json(sc)), '[]')
  INTO v_scorecards
  FROM (
    SELECT id, battle_id, contender_id, rubric_criterion_id, result, explanation
    FROM battles.scorecards
    WHERE battle_id = v_battle_id
  ) sc;

  SELECT COALESCE(jsonb_agg(row_to_json(cr)), '[]')
  INTO v_criteria
  FROM (
    SELECT id, title AS name, description, weight
    FROM battles.rubric_criteria
    WHERE id IN (
      SELECT rubric_criterion_id FROM battles.scorecards WHERE battle_id = v_battle_id
    )
  ) cr;

  SELECT COALESCE(jsonb_agg(row_to_json(v)), '[]')
  INTO v_verdicts
  FROM (
    SELECT id, contender_id, criterion_id, score, rationale, model_key, run_id, created_at
    FROM battles.ai_judge_verdicts
    WHERE battle_id = v_battle_id
  ) v;

  RETURN jsonb_build_object(
    'battle',       row_to_json(v_battle),
    'contenders',   v_contenders,
    'submissions',  v_submissions,
    'aggregates',   v_aggregates,
    'scorecards',   v_scorecards,
    'criteria',     v_criteria,
    'verdicts',     v_verdicts
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "rubric_criterion_id" "uuid", "result" "text", "explanation" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT sc.id, sc.battle_id, sc.contender_id, sc.rubric_criterion_id,
         sc.result, sc.explanation
  FROM battles.scorecards sc
  WHERE sc.battle_id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") RETURNS TABLE("battle_id" "uuid", "slug" "text", "title" "text", "status" "text", "total_vote_count" integer, "finalized_at" timestamp with time zone, "winner_contender_id" "uuid", "deleted_at" timestamp with time zone, "contender_a_id" "uuid", "contender_a_name" "text", "contender_b_id" "uuid", "contender_b_name" "text", "elo_winner_before" numeric, "elo_winner_after" numeric, "elo_loser_before" numeric, "elo_loser_after" numeric, "elo_is_draw" boolean)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'reputation'
    AS $$
DECLARE
  v_battle_id uuid;
  v_status    text;
  v_deleted   timestamptz;
BEGIN
  SELECT b.id, b.status, b.deleted_at
  INTO v_battle_id, v_status, v_deleted
  FROM battles.battles b
  WHERE b.slug = p_slug
  LIMIT 1;

  IF NOT FOUND OR v_deleted IS NOT NULL OR v_status = 'draft' THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT
    b.id, b.slug, b.title, b.status, b.total_vote_count, b.finalized_at,
    b.winner_contender_id, b.deleted_at,
    a_c.id, a_c.display_name,
    b_c.id, b_c.display_name,
    el.winner_score_before, el.winner_score_after,
    el.loser_score_before, el.loser_score_after,
    el.is_draw
  FROM battles.battles b
  LEFT JOIN battles.contenders a_c ON a_c.battle_id = b.id AND a_c.slot = 'A'
  LEFT JOIN battles.contenders b_c ON b_c.battle_id = b.id AND b_c.slot = 'B'
  LEFT JOIN reputation.elo_battle_log el ON el.battle_id = b.id
  WHERE b.id = v_battle_id
  LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "content_text" "text", "content_url" "text", "status" "text", "submitted_at" timestamp with time zone, "execution_run_id" "uuid", "is_final" boolean, "media_url" "text", "mime_type" "text", "output_modality" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT s.id, s.battle_id, s.contender_id, s.content_text, s.content_url,
         s.status::text, s.submitted_at, s.execution_run_id, s.is_final,
         s.media_url, s.mime_type, s.output_modality
  FROM battles.submissions s
  JOIN battles.battles b ON b.id = s.battle_id
  WHERE s.battle_id = p_battle_id
    AND b.deleted_at IS NULL
    AND (
      b.status::text = 'published'   -- anon: only published results are public
      OR auth.uid() IS NOT NULL      -- authenticated: any non-deleted battle is accessible
    );
$$;


ALTER FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battles_feed"("p_status" "text" DEFAULT NULL::"text", "p_battle_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "published_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "total_vote_count" integer, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "contender_a_id" "uuid", "contender_a_name" "text", "contender_a_type" "text", "contender_b_id" "uuid", "contender_b_name" "text", "contender_b_type" "text", "winner_slot" "text", "content_type" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    v.id,
    v.slug,
    v.title,
    v.status::text,
    v.published_at,
    v.battle_type::text,
    v.voter_eligibility::text,
    v.total_vote_count,
    v.voting_opens_at,
    v.voting_closes_at,
    v.contender_a_id,
    v.contender_a_name,
    v.contender_a_type::text,
    v.contender_b_id,
    v.contender_b_name,
    v.contender_b_type::text,
    v.winner_slot,
    v.content_type
  FROM battles.v_battle_feed_item v
  WHERE
    -- Defence-in-depth: always exclude draft/open regardless of p_status parameter
    v.status::text IN ('voting', 'scoring', 'closed', 'published', 'archived')
    AND (p_status      IS NULL OR v.status::text      = p_status)
    AND (p_battle_type IS NULL OR v.battle_type::text = p_battle_type)
    AND (p_cursor      IS NULL OR v.published_at      < p_cursor)
  ORDER BY v.published_at DESC NULLS LAST
  LIMIT LEAST(COALESCE(p_limit, 20), 100);
$$;


ALTER FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Paginated battle feed. Filters by status and battle_type (both optional text). Always excludes draft/open battles. Keyset pagination via p_cursor (published_at of last item). LIMIT capped at 100. Returns content_type for client-side category badges. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "category" "text", "elo_rating" numeric, "uncertainty" numeric, "battles_played" integer, "wins" integer, "draws" integer, "losses" integer, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT cr.id, cr.lenser_id, cr.category, cr.elo_rating, cr.uncertainty,
         cr.battles_played, cr.wins, cr.draws, cr.losses, cr.updated_at
  FROM reputation.contender_ratings cr
  WHERE cr.lenser_id = p_lenser_id
    AND (p_category IS NULL OR cr.category = p_category)
  ORDER BY cr.updated_at DESC
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") IS 'Security wrapper: fetch the most recent ELO contender rating for a lenser, optionally filtered to a specific category.';



CREATE OR REPLACE FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT to_jsonb(r.*)
  FROM public.fn_get_contender_rating(p_lenser_id) r
  LIMIT LEAST(GREATEST(p_limit, 1), 100);
$$;


ALTER FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer DEFAULT 30) RETURNS TABLE("day" "date", "battles" bigint, "wins" bigint, "votes_received" bigint, "xp_earned" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'xp', 'public'
    AS $$
  WITH date_series AS (
    SELECT generate_series(
      CURRENT_DATE - (p_days - 1) * INTERVAL '1 day',
      CURRENT_DATE,
      INTERVAL '1 day'
    )::date AS day
  ),
  battle_stats AS (
    SELECT
      b.published_at::date       AS day,
      COUNT(*)                   AS battles,
      COUNT(*) FILTER (
        WHERE b.winner_contender_id = c.id
      )                          AS wins
    FROM battles.contenders c
    JOIN battles.battles b ON b.id = c.battle_id
    WHERE c.contender_ref_id = p_lenser_id
      AND b.status IN ('closed', 'published')
      AND b.published_at >= CURRENT_DATE - p_days * INTERVAL '1 day'
    GROUP BY 1
  ),
  vote_stats AS (
    SELECT
      v.created_at::date AS day,
      COUNT(*)           AS votes_received
    FROM battles.votes v
    JOIN battles.contenders c ON c.id = v.voted_contender_id
    WHERE c.contender_ref_id = p_lenser_id
      AND v.created_at >= CURRENT_DATE - p_days * INTERVAL '1 day'
    GROUP BY 1
  ),
  xp_stats AS (
    SELECT
      x.created_at::date AS day,
      COALESCE(SUM(x.xp), 0) AS xp_earned
    FROM xp.events x
    WHERE x.lenser_id = p_lenser_id
      AND x.created_at >= CURRENT_DATE - p_days * INTERVAL '1 day'
    GROUP BY 1
  )
  SELECT
    ds.day,
    COALESCE(bs.battles, 0)        AS battles,
    COALESCE(bs.wins, 0)           AS wins,
    COALESCE(vs.votes_received, 0) AS votes_received,
    COALESCE(xs.xp_earned, 0)      AS xp_earned
  FROM date_series ds
  LEFT JOIN battle_stats bs ON bs.day = ds.day
  LEFT JOIN vote_stats    vs ON vs.day = ds.day
  LEFT JOIN xp_stats      xs ON xs.day = ds.day
  ORDER BY ds.day ASC
$$;


ALTER FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) IS 'Returns a daily timeseries of battles, wins, votes received, and XP earned for a given lenser over the last p_days days. SECURITY DEFINER — no RLS bypass needed.';



CREATE OR REPLACE FUNCTION "public"."fn_get_dlq_counts"() RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lenses'
    AS $$
  SELECT jsonb_build_object(
    'battle_dlq_count',
    (SELECT COUNT(*) FROM battles.battle_execution_dead_letters WHERE resolved_at IS NULL),
    'workflow_dlq_count',
    (SELECT COUNT(*) FROM lenses.workflow_run_dead_letters WHERE resolved_at IS NULL)
  );
$$;


ALTER FUNCTION "public"."fn_get_dlq_counts"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_unresolved_only" boolean DEFAULT false, "p_limit" integer DEFAULT 50) RETURNS TABLE("id" "uuid", "job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "error_code" "text", "error_message" "text", "attempt_count" integer, "payload" "jsonb", "resolved_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT dl.id, dl.job_id, dl.battle_id, dl.contender_id, dl.slot,
         dl.error_code, dl.error_message, dl.attempt_count, dl.payload,
         dl.resolved_at, dl.created_at
  FROM battles.battle_execution_dead_letters dl
  WHERE (p_battle_id IS NULL       OR dl.battle_id = p_battle_id)
    AND (NOT p_unresolved_only     OR dl.resolved_at IS NULL)
  ORDER BY dl.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("attachment_id" "uuid", "object_id" "uuid", "entity_type" "text", "entity_id" "uuid", "binding_key" "text", "attached_at" timestamp with time zone, "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'media'
    AS $$
  SELECT
    a.id           AS attachment_id,
    a.object_id,
    a.entity_type,
    a.entity_id,
    a.binding_key,
    a.attached_at,
    o.bucket, o.object_key, o.external_url, o.mime_type, o.media_type,
    o.name, o.byte_size, o.visibility, o.lifecycle_state, o.metadata
  FROM media.attachments a
  JOIN media.objects o ON o.id = a.object_id
  WHERE a.entity_type = p_entity_type
    AND a.entity_id   = p_entity_id;
$$;


ALTER FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: list media attachments for an entity with nested object metadata.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT COALESCE(
    jsonb_object_agg(r.reaction, r.cnt),
    '{}'::jsonb
  )
  FROM (
    SELECT reaction, COUNT(*) AS cnt
    FROM content.reactions
    WHERE entity_type::text = p_entity_type
      AND entity_id   = p_entity_id
    GROUP BY reaction
  ) r;
$$;


ALTER FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: return a jsonb map of reaction → count for a single entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("reaction" "text", "reacted" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT
    rt.reaction::text,
    EXISTS (
      SELECT 1
      FROM content.reactions r
      WHERE r.entity_type::text = p_entity_type
        AND r.entity_id         = p_entity_id
        AND r.lenser_id         = lensers.get_auth_lenser_id()
        AND r.reaction          = rt.reaction
    ) AS reacted
  FROM unnest(enum_range(NULL::content.reaction_enum)) rt(reaction);
$$;


ALTER FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Returns one row per reaction type with reacted=true if the current user reacted. Used by reactionRepository.ts getReactions(). Requires authentication.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid" DEFAULT NULL::"uuid", "p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "entity_type" "text", "entity_id" "uuid", "lenser_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT
    r.id,
    r.entity_type::text,
    r.entity_id,
    r.lenser_id,
    r.reaction::text,
    r.created_at
  FROM content.reactions r
  WHERE r.entity_type::text = p_entity_type
    AND (p_entity_id IS NULL OR r.entity_id = p_entity_id)
    AND (p_lenser_id IS NULL OR r.lenser_id = p_lenser_id);
$$;


ALTER FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") IS 'Flexible reaction query: filter by entity_type (required), entity_id (optional), and/or lenser_id (optional). Used by reactionRepository getReactionsFor, getBatchUserReactions, and getLenserHistory.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("tag_id" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT tm.tag_id
  FROM content.tag_map tm
  WHERE tm.entity_type::text = p_entity_type
    AND tm.entity_id   = p_entity_id;
$$;


ALTER FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: return tag UUIDs mapped to an entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("title" "text", "description" "text", "content" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT et.title, et.description, et.content
  FROM content.entity_translations et
  WHERE et.entity_type::text = p_entity_type
    AND et.entity_id   = p_entity_id
    AND et.is_original = true
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: fetch the original (is_original=true) translation for any entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(eb.*)
  FROM agents.evaluation_baselines eb
  JOIN agents.evaluations ev ON ev.id = eb.evaluation_id
  WHERE eb.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(erv.*)
  FROM agents.evaluation_results_v erv
  JOIN agents.evaluation_runs er ON er.id = erv.run_id
  JOIN agents.evaluations ev ON ev.id = er.evaluation_id
  WHERE erv.run_id = p_run_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin());
$$;


ALTER FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "run_id" "uuid", "artifact_kind" "text", "content_text" "text", "content_json" "jsonb", "visibility" "text", "is_primary_output" boolean, "output_type" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers'
    AS $$
  SELECT
    a.id, a.run_id, a.artifact_kind, a.content_text, a.content_json,
    a.visibility, a.is_primary_output, a.output_type, a.created_at
  FROM execution.artifacts a
  WHERE a.run_id = p_run_id
    AND EXISTS (
      SELECT 1 FROM execution.fn_get_run_details(p_run_id) LIMIT 1
    )
  ORDER BY a.is_primary_output DESC, a.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") IS 'Security wrapper: list artifacts for a run. Access gated via execution.fn_get_run_details — returns empty if caller cannot see the run.';



CREATE OR REPLACE FUNCTION "public"."fn_get_execution_status"() RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'admin', 'platform', 'lenses', 'battles', 'public'
    AS $$
  SELECT jsonb_build_object(
    'system_kill_switch_active',
      public.fn_kill_switch_active('system'),
    'queue_frozen',
      (SELECT queue_frozen  FROM admin.execution_control WHERE id = 1),
    'frozen_reason',
      (SELECT frozen_reason FROM admin.execution_control WHERE id = 1),
    'active_run_count',
      (SELECT COUNT(*) FROM lenses.workflow_runs
       WHERE status IN ('running', 'streaming', 'recovered')),
    'queued_run_count',
      (SELECT COUNT(*) FROM lenses.workflow_runs
       WHERE status IN ('queued', 'pending')),
    'active_battle_job_count',
      (SELECT COUNT(*) FROM battles.battle_execution_jobs
       WHERE status IN ('claimed', 'running')),
    'queued_battle_job_count',
      (SELECT COUNT(*) FROM battles.battle_execution_jobs
       WHERE status = 'queued'),
    'active_worker_count',
      (SELECT COUNT(*) FROM platform.api_worker_heartbeats
       WHERE last_seen_at >= now() - interval '2 minutes'),
    'stale_worker_count',
      (SELECT COUNT(*) FROM platform.api_worker_heartbeats
       WHERE last_seen_at < now() - interval '2 minutes'),
    'dlq_workflow_count',
      (SELECT COUNT(*) FROM lenses.workflow_run_dead_letters
       WHERE resolved_at IS NULL),
    'dlq_battle_count',
      (SELECT COUNT(*) FROM battles.battle_execution_dead_letters
       WHERE resolved_at IS NULL)
  );
$$;


ALTER FUNCTION "public"."fn_get_execution_status"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_execution_status"() IS 'Global execution health dashboard. Returns system_kill_switch_active, queue_frozen, active/queued run/job counts, worker health, and DLQ depths. Granted to authenticated (not admin-only) so frontend banners work for all users.';



CREATE OR REPLACE FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(fo.*)
  FROM agents.v_human_fleet_overview fo
  WHERE fo.human_lenser_id = p_human_lenser_id
    AND (p_human_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") RETURNS "text"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT COALESCE(
    (SELECT
       CASE r.status
         WHEN 'pending'  THEN 'pending'
         WHEN 'accepted' THEN 'following'
       END
     FROM lensers.relationships r
     JOIN lensers.profiles src ON src.id = r.source_profile_id AND src.user_id = auth.uid()
     WHERE r.target_profile_id = p_target_profile_id
       AND r.removed_at IS NULL
     LIMIT 1),
    'none'
  )
$$;


ALTER FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_gateway_device_health"() RETURNS TABLE("device_id" "uuid", "hostname" "text", "daemon_version" "text", "last_seen_at" timestamp with time zone, "approved_at" timestamp with time zone, "revoked_at" timestamp with time zone, "kill_switch" boolean, "created_at" timestamp with time zone, "pending_commands" bigint, "unacked_commands" bigint)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
DECLARE
  v_uid UUID := auth.uid();
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
    SELECT v.device_id, v.hostname, v.daemon_version, v.last_seen_at,
           v.approved_at, v.revoked_at, v.kill_switch, v.created_at,
           v.pending_commands, v.unacked_commands
      FROM agents.v_gateway_device_health v
     WHERE v.owner_id = v_uid
     ORDER BY v.last_seen_at DESC NULLS LAST,
              v.created_at DESC;
END $$;


ALTER FUNCTION "public"."fn_get_gateway_device_health"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer DEFAULT 50) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_wins" bigint, "total_battles" bigint, "win_rate" numeric, "total_votes_received" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'reputation'
    AS $$
  SELECT
    lp.id                                                                  AS lenser_id,
    lp.handle,
    lp.display_name,
    lp.avatar_url,
    COUNT(c.id) FILTER (WHERE b.winner_contender_id = c.id)               AS total_wins,
    COUNT(c.id)                                                            AS total_battles,
    CASE WHEN COUNT(c.id) > 0
         THEN ROUND(
           COUNT(c.id) FILTER (WHERE b.winner_contender_id = c.id)::numeric
           / COUNT(c.id)::numeric, 4)
         ELSE 0::numeric
    END                                                                    AS win_rate,
    COALESCE(SUM(va.raw_vote_count), 0)                                    AS total_votes_received
  FROM lensers.profiles lp
  JOIN battles.contenders c ON c.contender_ref_id = lp.id
  JOIN battles.battles b
    ON b.id = c.battle_id AND b.status::text IN ('published', 'closed')
  LEFT JOIN battles.vote_aggregates va ON va.contender_id = c.id
  GROUP BY lp.id, lp.handle, lp.display_name, lp.avatar_url
  HAVING COUNT(c.id) > 0
  ORDER BY total_wins DESC, total_battles DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer DEFAULT 50, "p_before_ts" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_before_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "sender_id" "uuid", "sender_handle" "text", "sender_role" "text", "body" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    id,
    battle_id,
    sender_id,
    sender_handle,
    sender_role,
    body,
    created_at
  FROM battles.global_messages
  WHERE battle_id = p_battle_id
    AND (
      p_before_ts IS NULL
      OR (created_at, id) < (p_before_ts, p_before_id)
    )
  ORDER BY created_at DESC, id DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") RETURNS TABLE("total_battles" bigint, "a_wins" bigint, "b_wins" bigint, "draws" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  WITH shared_battles AS (
    -- Battles where both lensers participated
    SELECT ca.battle_id
    FROM battles.contenders ca
    JOIN battles.contenders cb ON cb.battle_id = ca.battle_id
      AND cb.contender_ref_id = p_lenser_b
    WHERE ca.contender_ref_id = p_lenser_a
  ),
  outcomes AS (
    SELECT
      b.id AS battle_id,
      MAX(CASE WHEN c.contender_ref_id = p_lenser_a AND b.winner_contender_id = c.id THEN 1 ELSE 0 END) AS a_won,
      MAX(CASE WHEN c.contender_ref_id = p_lenser_b AND b.winner_contender_id = c.id THEN 1 ELSE 0 END) AS b_won
    FROM shared_battles sb
    JOIN battles.battles b ON b.id = sb.battle_id
    JOIN battles.contenders c ON c.battle_id = b.id
      AND c.contender_ref_id IN (p_lenser_a, p_lenser_b)
    WHERE b.status IN ('closed', 'published')
    GROUP BY b.id
  )
  SELECT
    COUNT(*)                               AS total_battles,
    SUM(CASE WHEN a_won = 1 AND b_won = 0 THEN 1 ELSE 0 END) AS a_wins,
    SUM(CASE WHEN b_won = 1 AND a_won = 0 THEN 1 ELSE 0 END) AS b_wins,
    SUM(CASE WHEN a_won = b_won           THEN 1 ELSE 0 END) AS draws
  FROM outcomes
$$;


ALTER FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") IS 'Returns aggregate win/loss/draw stats for two lensers across all battles where both participated. SECURITY DEFINER — caller visibility is public.';



CREATE OR REPLACE FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("occurred_at" timestamp with time zone, "kind" "text", "ai_lenser_id" "uuid", "ai_lenser_handle" "text", "ai_lenser_name" "text", "title" "text", "status" "text", "team_run_id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "action_type" "text", "payload" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_caller uuid;
  v_safe_limit integer := COALESCE(p_limit, 50);
  v_safe_offset integer := COALESCE(p_offset, 0);
BEGIN
  v_caller := lensers.get_auth_human_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  IF v_caller <> p_human_lenser_id THEN
    RAISE EXCEPTION 'Forbidden: cannot read another human Lenser''s activity feed.'
      USING ERRCODE = '42501';
  END IF;

  IF v_safe_limit < 1 THEN v_safe_limit := 1; END IF;
  IF v_safe_limit > 200 THEN v_safe_limit := 200; END IF;
  IF v_safe_offset < 0 THEN v_safe_offset := 0; END IF;

  RETURN QUERY
  WITH owned_agents AS (
    SELECT o.ai_lenser_id, p.handle, p.display_name
    FROM agents.ownerships o
    JOIN agents.ai_lensers al ON al.id = o.ai_lenser_id
    JOIN lensers.profiles p ON p.id = al.profile_id
    WHERE o.owner_lenser_id = v_caller
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  ),
  approval_items AS (
    SELECT
      tr.created_at AS occurred_at,
      'approval_pending'::text AS kind,
      tr.ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      COALESCE(w.title, 'Untitled workflow') AS title,
      'pending'::text AS status,
      tr.id AS team_run_id,
      tr.workflow_id,
      NULL::uuid AS schedule_id,
      NULL::text AS action_type,
      jsonb_build_object(
        'gate_kind', tr.metadata->>'gate_kind',
        'requested_action', tr.metadata->>'requested_action',
        'requester_agent_id', tr.metadata->>'requester_agent_id'
      ) AS payload
    FROM agents.team_runs tr
    JOIN owned_agents oa ON oa.ai_lenser_id = tr.ai_lenser_id
    LEFT JOIN lenses.workflows w ON w.id = tr.workflow_id
    WHERE tr.approval_status = 'pending'
  ),
  run_items AS (
    SELECT
      COALESCE(tr.completed_at, tr.started_at, tr.created_at) AS occurred_at,
      'team_run'::text AS kind,
      tr.ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      COALESCE(w.title, 'Untitled workflow') AS title,
      tr.status,
      tr.id AS team_run_id,
      tr.workflow_id,
      NULL::uuid AS schedule_id,
      NULL::text AS action_type,
      jsonb_build_object(
        'approval_status', tr.approval_status,
        'started_at', tr.started_at,
        'completed_at', tr.completed_at
      ) AS payload
    FROM agents.team_runs tr
    JOIN owned_agents oa ON oa.ai_lenser_id = tr.ai_lenser_id
    LEFT JOIN lenses.workflows w ON w.id = tr.workflow_id
    WHERE tr.approval_status <> 'pending'
  ),
  schedule_items AS (
    SELECT
      COALESCE(s.last_run_at, s.created_at) AS occurred_at,
      'schedule_dispatch'::text AS kind,
      al.id AS ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      COALESCE(w.title, 'Untitled workflow') AS title,
      COALESCE(s.last_dispatch_status, 'never_dispatched') AS status,
      NULL::uuid AS team_run_id,
      s.workflow_id,
      s.id AS schedule_id,
      NULL::text AS action_type,
      jsonb_build_object(
        'cron_expr', s.cron_expr,
        'timezone', s.timezone,
        'next_run_at', s.next_run_at,
        'last_error_message', s.last_error_message,
        'assignee_type', s.assignee_type,
        'assignee_id', s.assignee_id
      ) AS payload
    FROM lenses.workflow_schedules s
    JOIN lenses.workflows w ON w.id = s.workflow_id
    -- Schedules are bound to a workflow whose lenser_id is the AI workspace
    -- profile id when assignee_type='agent'; otherwise we resolve via the
    -- assignee_id column.
    JOIN agents.ai_lensers al
      ON al.id = COALESCE(
           CASE WHEN s.assignee_type = 'agent' THEN s.assignee_id END,
           (SELECT id FROM agents.ai_lensers ail WHERE ail.profile_id = w.lenser_id)
         )
    JOIN owned_agents oa ON oa.ai_lenser_id = al.id
  ),
  action_items AS (
    SELECT
      a.occurred_at,
      'agent_action'::text AS kind,
      a.ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      a.action_type AS title,
      a.result AS status,
      NULL::uuid AS team_run_id,
      NULL::uuid AS workflow_id,
      NULL::uuid AS schedule_id,
      a.action_type,
      a.metadata AS payload
    FROM agents.action_logs a
    JOIN owned_agents oa ON oa.ai_lenser_id = a.ai_lenser_id
  ),
  combined AS (
    SELECT * FROM approval_items
    UNION ALL
    SELECT * FROM run_items
    UNION ALL
    SELECT * FROM schedule_items
    UNION ALL
    SELECT * FROM action_items
  )
  SELECT *
  FROM combined
  ORDER BY occurred_at DESC NULLS LAST
  LIMIT v_safe_limit OFFSET v_safe_offset;
END;
$$;


ALTER FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Cross-agent activity feed for the calling human Lenser. Aggregates pending approvals, recent team runs, schedule dispatches, and agent actions across every owned AI lenser. Caller must equal p_human_lenser_id.';



CREATE OR REPLACE FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "calibration_score" numeric, "total_judgments" integer, "agreement_rate" numeric, "kappa_score" numeric, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT jc.id, jc.lenser_id, jc.calibration_score, jc.total_judgments,
         jc.agreement_rate, jc.kappa_score, jc.updated_at
  FROM reputation.judge_calibrations jc
  WHERE jc.lenser_id = p_lenser_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") IS 'Security wrapper: return the judge calibration record for a lenser.';



CREATE OR REPLACE FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT to_jsonb(b.*)
  FROM battles.battles b
  WHERE b.workflow_id = p_workflow_id
    AND b.status::text = 'draft'
    AND b.creator_lenser_id = lensers.get_auth_lenser_id()
  ORDER BY b.created_at DESC
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lensers"."profiles" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "user_id" "uuid" DEFAULT "auth"."uid"(),
    "handle" "text" NOT NULL,
    "display_name" "text" NOT NULL,
    "bio" "text",
    "headline" "text",
    "avatar_url" "text",
    "banner_url" "text",
    "location" "text",
    "website_url" "text",
    "status" "lensers"."lenser_status" DEFAULT 'active'::"lensers"."lenser_status" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "visibility" "lensers"."lenser_visibility" DEFAULT 'public'::"lensers"."lenser_visibility",
    "is_in_waiting_list" boolean DEFAULT false,
    "deletion_requested_at" timestamp with time zone,
    "last_handle_changed_at" timestamp with time zone,
    "join_order" bigint,
    "last_login_at" timestamp with time zone,
    "last_active_at" timestamp with time zone,
    "login_count" integer DEFAULT 0,
    "onboarding_step" smallint DEFAULT 0 NOT NULL,
    "onboarding_completed_at" timestamp with time zone,
    "type" "lensers"."lenser_type" DEFAULT 'human'::"lensers"."lenser_type" NOT NULL,
    "ai_model_id" "uuid",
    "deletion_deadline_at" timestamp with time zone,
    "is_super_admin" boolean DEFAULT false NOT NULL,
    "journey_state" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "referral_source" "text",
    CONSTRAINT "ck_lensers_profiles_bio_len" CHECK ((("bio" IS NULL) OR ("length"("bio") <= 500))),
    CONSTRAINT "ck_lensers_profiles_display_name_len" CHECK ((("display_name" IS NULL) OR ("length"("display_name") <= 60))),
    CONSTRAINT "ck_lensers_profiles_headline_len" CHECK ((("headline" IS NULL) OR ("length"("headline") <= 120))),
    CONSTRAINT "ck_lensers_profiles_login_count_nonneg" CHECK ((("login_count" IS NULL) OR ("login_count" >= 0))),
    CONSTRAINT "lensers_handle_format_check" CHECK ((("handle" = "lower"("handle")) AND ("handle" ~ '^[a-z0-9._]+$'::"text") AND (("char_length"("handle") >= 4) AND ("char_length"("handle") <= 24)))),
    CONSTRAINT "lensers_website_url_format_check" CHECK ((("website_url" IS NULL) OR ("website_url" ~ '^https?:\/\/[A-Za-z0-9.-]+\.[A-Za-z]{2,}.*$'::"text")))
);


ALTER TABLE "lensers"."profiles" OWNER TO "postgres";


COMMENT ON COLUMN "lensers"."profiles"."onboarding_step" IS '0=not started, 1=handle/profile created, 2=preferences set (complete)';



COMMENT ON COLUMN "lensers"."profiles"."onboarding_completed_at" IS 'Timestamp when all onboarding steps were finished; NULL means incomplete.';



COMMENT ON COLUMN "lensers"."profiles"."journey_state" IS 'Per-step boolean map written by fn_journey_state_mark; keys match OnboardingStepId product journey steps.';



COMMENT ON COLUMN "lensers"."profiles"."referral_source" IS 'UTM / referral token captured at account creation from a battle invite link.';



CREATE TABLE IF NOT EXISTS "reputation"."contender_ratings" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "category" "text" NOT NULL,
    "elo_rating" numeric(10,4) DEFAULT 1000 NOT NULL,
    "uncertainty" numeric(10,4) DEFAULT 350 NOT NULL,
    "battles_played" integer DEFAULT 0 NOT NULL,
    "wins" integer DEFAULT 0 NOT NULL,
    "draws" integer DEFAULT 0 NOT NULL,
    "losses" integer DEFAULT 0 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "sigma" numeric(10,6) DEFAULT 0.06 NOT NULL,
    "tau" numeric(10,6) DEFAULT 0.0833 NOT NULL,
    "beta" numeric(10,6) DEFAULT 4.1667 NOT NULL,
    CONSTRAINT "ck_reputation_contender_ratings_elo_rating_nonneg" CHECK ((("elo_rating" IS NULL) OR ("elo_rating" >= (0)::numeric))),
    CONSTRAINT "contender_ratings_draws_nonneg" CHECK (("draws" >= 0)),
    CONSTRAINT "contender_ratings_losses_nonneg" CHECK (("losses" >= 0)),
    CONSTRAINT "contender_ratings_played_nonneg" CHECK (("battles_played" >= 0)),
    CONSTRAINT "contender_ratings_wins_nonneg" CHECK (("wins" >= 0))
);


ALTER TABLE "reputation"."contender_ratings" OWNER TO "postgres";


COMMENT ON TABLE "reputation"."contender_ratings" IS 'Per-category ELO battle performance. One row per (lenser_id, category). battles_played = wins + draws + losses must hold after each update (enforced by the trigger, not a CHECK constraint). category is free text to avoid enum rigidity; standardize via app logic.';



COMMENT ON COLUMN "reputation"."contender_ratings"."sigma" IS 'Glicko-2 volatility σ (default 0.06). Updated after each battle via fn_update_elo_on_finalize.';



COMMENT ON COLUMN "reputation"."contender_ratings"."tau" IS 'TrueSkill dynamics factor (sigma/3): models skill drift over time between battles.';



COMMENT ON COLUMN "reputation"."contender_ratings"."beta" IS 'TrueSkill performance variance: models the randomness of a single game performance.';



CREATE TABLE IF NOT EXISTS "reputation"."lenser_scores" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "score_type" "text" NOT NULL,
    "score" numeric(10,4) DEFAULT 1000 NOT NULL,
    "uncertainty" numeric(10,4) DEFAULT 350 NOT NULL,
    "computed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "lenser_scores_type_check" CHECK (("score_type" = ANY (ARRAY['elo'::"text", 'trust'::"text", 'composite'::"text"])))
);


ALTER TABLE "reputation"."lenser_scores" OWNER TO "postgres";


COMMENT ON TABLE "reputation"."lenser_scores" IS 'Overall reputation score per lenser. One row per (lenser_id, score_type). Updated by service_role after battle finalization or scheduled recomputation. uncertainty is the Glicko-2 rating deviation (RD): lower = more confident.';



CREATE TABLE IF NOT EXISTS "xp"."totals" (
    "lenser_id" "uuid" NOT NULL,
    "app_id" "uuid" NOT NULL,
    "total_xp" bigint DEFAULT 0 NOT NULL,
    "current_level" integer DEFAULT 1 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_xp_totals_total_xp_nonneg" CHECK ((("total_xp" IS NULL) OR ("total_xp" >= 0)))
);


ALTER TABLE "xp"."totals" OWNER TO "postgres";


CREATE OR REPLACE VIEW "xp"."v_leaderboard" AS
 SELECT "p"."id" AS "lenser_id",
    "p"."handle",
    "p"."display_name",
    "p"."avatar_url",
    "p"."type" AS "lenser_type",
    COALESCE("xt"."total_xp", (0)::bigint) AS "total_xp",
    COALESCE("xt"."current_level", 1) AS "current_level",
    COALESCE("rs_elo"."score", (1000)::numeric) AS "elo_score",
    COALESCE("rs_cmp"."score", (1000)::numeric) AS "composite_score",
    COALESCE("sum"("cr"."battles_played"), (0)::bigint) AS "battles_played",
    COALESCE("sum"("cr"."wins"), (0)::bigint) AS "battles_won",
    "dense_rank"() OVER (ORDER BY COALESCE("xt"."total_xp", (0)::bigint) DESC) AS "xp_rank",
    "dense_rank"() OVER (ORDER BY COALESCE("rs_elo"."score", (1000)::numeric) DESC) AS "elo_rank"
   FROM (((("lensers"."profiles" "p"
     LEFT JOIN "xp"."totals" "xt" ON (("xt"."lenser_id" = "p"."id")))
     LEFT JOIN "reputation"."lenser_scores" "rs_elo" ON ((("rs_elo"."lenser_id" = "p"."id") AND ("rs_elo"."score_type" = 'elo'::"text"))))
     LEFT JOIN "reputation"."lenser_scores" "rs_cmp" ON ((("rs_cmp"."lenser_id" = "p"."id") AND ("rs_cmp"."score_type" = 'composite'::"text"))))
     LEFT JOIN "reputation"."contender_ratings" "cr" ON (("cr"."lenser_id" = "p"."id")))
  WHERE (("p"."status" = 'active'::"lensers"."lenser_status") AND ("p"."visibility" = 'public'::"lensers"."lenser_visibility") AND ("p"."deletion_requested_at" IS NULL))
  GROUP BY "p"."id", "p"."handle", "p"."display_name", "p"."avatar_url", "p"."type", "xt"."total_xp", "xt"."current_level", "rs_elo"."score", "rs_cmp"."score";


ALTER VIEW "xp"."v_leaderboard" OWNER TO "postgres";


COMMENT ON VIEW "xp"."v_leaderboard" IS 'XP + ELO leaderboard for LeaderboardPage. DENSE_RANK() computed server-side. Supports both xp_rank and elo_rank tabs. Active public profiles only. Paginate by total_xp DESC with cursor.';



CREATE OR REPLACE FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text" DEFAULT 'xp'::"text", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS SETOF "xp"."v_leaderboard"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'reputation', 'lensers', 'public'
    AS $$
BEGIN
  IF p_order_by = 'elo' THEN
    RETURN QUERY
      SELECT * FROM xp.v_leaderboard
      ORDER BY elo_score DESC, total_xp DESC, lenser_id
      LIMIT  LEAST(COALESCE(p_limit, 50), 100)
      OFFSET GREATEST(COALESCE(p_offset, 0), 0);
  ELSE
    RETURN QUERY
      SELECT * FROM xp.v_leaderboard
      ORDER BY total_xp DESC, elo_score DESC, lenser_id
      LIMIT  LEAST(COALESCE(p_limit, 50), 100)
      OFFSET GREATEST(COALESCE(p_offset, 0), 0);
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) IS 'Paginated leaderboard for LeaderboardPage. p_order_by: ''xp'' (default) sorts by total_xp, ''elo'' sorts by elo_score. LIMIT capped at 100. Uses offset pagination (simple for leaderboard UX).';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "battle_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "assigned_at" timestamp with time zone, "input_snapshot" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT la.id, la.contender_id, la.battle_id, la.lens_id, la.version_id,
         la.assigned_at, la.input_snapshot
  FROM battles.contender_lens_assignments la
  WHERE la.contender_id = p_contender_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_viewer_id       uuid;
  v_lens            RECORD;
  v_translation     RECORD;
  v_author          RECORD;
  v_tags            jsonb;
  v_reaction_totals jsonb;
  v_user_reactions  jsonb;
  v_latest_ver      RECORD;
  v_params          jsonb;
BEGIN
  v_viewer_id := lensers.get_auth_lenser_id();

  -- Core lens: visibility check (owner sees own, everyone sees public)
  SELECT l.id, l.lenser_id, l.visibility::text, l.parent_lens_id,
         l.forked_from_execution_id, l.head_version_id,
         l.created_at, l.updated_at
  INTO v_lens
  FROM lenses.lenses l
  WHERE l.id = p_lens_id
    AND (
      l.visibility = 'public'::content.visibility_enum
      OR l.lenser_id = v_viewer_id
    );

  IF NOT FOUND THEN
    RETURN jsonb_build_object('error', 'not_found');
  END IF;

  -- Translation: title, description, content (prefers original, then 'en')
  SELECT et.title, et.description, et.content
  INTO v_translation
  FROM content.entity_translations et
  WHERE et.entity_type = 'lens'::content.entity_type_enum
    AND et.entity_id   = p_lens_id
  ORDER BY et.is_original DESC, (et.language_code = 'en') DESC, et.created_at DESC
  LIMIT 1;

  -- Author profile
  SELECT p.id, p.handle, p.display_name, p.avatar_url
  INTO v_author
  FROM lensers.profiles p
  WHERE p.id = v_lens.lenser_id;

  -- Tags via content.tag_map + content.tags + content.tag_translations (en fallback -> slug)
  SELECT COALESCE(
    jsonb_agg(jsonb_build_object(
      'id',   t.id,
      'slug', t.slug,
      'name', COALESCE(tt_en.name, t.slug)
    )),
    '[]'::jsonb
  )
  INTO v_tags
  FROM content.tag_map tm
  JOIN content.tags t ON t.id = tm.tag_id
  LEFT JOIN content.tag_translations tt_en
    ON tt_en.tag_id = t.id AND tt_en.language_code = 'en'
  WHERE tm.entity_type::text = 'lens'
    AND tm.entity_id          = p_lens_id;

  -- Reaction counts
  SELECT COALESCE(
    jsonb_object_agg(r.reaction::text, r.cnt),
    '{}'::jsonb
  )
  INTO v_reaction_totals
  FROM (
    SELECT reaction, COUNT(*) AS cnt
    FROM content.reactions
    WHERE entity_type::text = 'lens'
      AND entity_id          = p_lens_id
    GROUP BY reaction
  ) r;

  -- Current user's own reactions (empty if not authenticated)
  IF v_viewer_id IS NOT NULL THEN
    SELECT COALESCE(
      jsonb_object_agg(r.reaction::text, true),
      '{}'::jsonb
    )
    INTO v_user_reactions
    FROM content.reactions r
    WHERE r.entity_type::text = 'lens'
      AND r.entity_id          = p_lens_id
      AND r.lenser_id          = v_viewer_id;
  ELSE
    v_user_reactions := '{}'::jsonb;
  END IF;

  -- Latest published version
  SELECT v.id, v.version_number, v.template_body, v.status::text,
         v.changelog, v.published_at, v.created_at
  INTO v_latest_ver
  FROM lenses.versions v
  WHERE v.lens_id    = p_lens_id
    AND v.status     = 'published'::content.content_status
  ORDER BY v.version_number DESC
  LIMIT 1;

  -- Version parameters for latest published (empty array if no published version)
  IF v_latest_ver.id IS NOT NULL THEN
    SELECT COALESCE(
      jsonb_agg(
        jsonb_build_object(
          'id',        vp.id,
          'label',     vp.label,
          'toolId',    vp.tool_id,
          'versionId', vp.version_id,
          'tool',      jsonb_build_object(
            'id',               tk.id,
            'key',              tk.key,
            'label',            tk.label,
            'description',      tk.description,
            'category',         tk.category,
            'type',             tk.type,
            'required',         tk.required,
            'min_length',       tk.min_length,
            'max_length',       tk.max_length,
            'placeholder',      tk.placeholder,
            'help_text',        tk.help_text,
            'validation_schema',tk.validation_schema,
            'options',          tk.options,
            'sort_order',       tk.sort_order,
            'is_system',        tk.is_system,
            'icon',             tk.icon,
            'color',            tk.color
          )
        )
        ORDER BY tk.sort_order, vp.id
      ),
      '[]'::jsonb
    )
    INTO v_params
    FROM lenses.version_parameters vp
    JOIN lenses.tools tk ON tk.id = vp.tool_id
    WHERE vp.version_id = v_latest_ver.id;
  ELSE
    v_params := '[]'::jsonb;
  END IF;

  RETURN jsonb_build_object(
    'id',                        v_lens.id,
    'lenser_id',                 v_lens.lenser_id,
    'visibility',                v_lens.visibility,
    'parent_lens_id',            v_lens.parent_lens_id,
    'forked_from_execution_id',  v_lens.forked_from_execution_id,
    'head_version_id',           v_lens.head_version_id,
    'created_at',                v_lens.created_at,
    'updated_at',                v_lens.updated_at,
    'title',                     COALESCE(v_translation.title, 'Untitled'),
    'description',               v_translation.description,
    'content',                   COALESCE(v_translation.content, ''),
    'author_profile',            jsonb_build_object(
                                   'id',           v_author.id,
                                   'handle',       v_author.handle,
                                   'display_name', v_author.display_name,
                                   'avatar_url',   v_author.avatar_url
                                 ),
    'tags',                      COALESCE(v_tags, '[]'::jsonb),
    'reaction_totals',           v_reaction_totals,
    'user_reactions',            v_user_reactions,
    'latest_published_version',  CASE
      WHEN v_latest_ver.id IS NOT NULL THEN jsonb_build_object(
        'id',             v_latest_ver.id,
        'lens_id',        p_lens_id,
        'version_number', v_latest_ver.version_number,
        'template_body',  v_latest_ver.template_body,
        'status',         v_latest_ver.status,
        'changelog',      v_latest_ver.changelog,
        'published_at',   v_latest_ver.published_at,
        'created_at',     v_latest_ver.created_at,
        'parameters',     COALESCE(v_params, '[]'::jsonb)
      )
      ELSE NULL
    END
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") IS 'Single-call bootstrap for LensDetailPage. Replaces 9 sequential RPCs with one roundtrip. Returns full lens detail + author + tags + reaction counts + user reactions + latest published version. STABLE = cacheable. Returns {"error":"not_found"} for inaccessible lenses.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("request_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "version_number" integer, "model_id" "uuid", "model_key" "text", "provider_key" "text", "funding_source" "text", "run_id" "uuid", "run_status" "text", "latency_ms" integer, "token_input" integer, "token_output" integer, "credit_cost" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lenses', 'ai', 'lensers', 'auth'
    AS $$
DECLARE
  v_limit  integer := LEAST(GREATEST(COALESCE(p_limit,  20), 1), 100);
  v_offset integer := GREATEST(COALESCE(p_offset, 0), 0);
BEGIN
  IF p_lens_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT *
  FROM execution.fn_get_lens_execution_history(p_lens_id, v_limit, v_offset);
END;
$$;


ALTER FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Public SECURITY DEFINER wrapper exposing execution.fn_get_lens_execution_history via PostgREST. Caps p_limit to 100, p_offset to >= 0. Returns empty set (not 404) for unknown lenses or unauthenticated callers — the inner function enforces auth.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") RETURNS TABLE("id" "uuid", "head_version_id" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT l.id, l.head_version_id
  FROM lenses.lenses l
  WHERE l.id = p_lens_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") IS 'Security wrapper: fetch a lens by ID and return its head_version_id. Used by the execution pipeline before queuing a run.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT COALESCE(
    jsonb_agg(to_jsonb(f.*) ORDER BY f.depth ASC),
    '[]'::jsonb
  )
  FROM lenses.vw_fork_history f
  WHERE f.lens_id = p_lens_id;
$$;


ALTER FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") IS 'Security wrapper: return the fork ancestry chain for a lens as a jsonb array ordered by depth ascending.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") RETURNS TABLE("id" "uuid", "lens_id" "uuid", "version_number" integer, "status" "text", "template_body" "text", "changelog" "text", "parent_version_id" "uuid", "published_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    v.id, v.lens_id, v.version_number, v.status::text,
    v.template_body, v.changelog, v.parent_version_id,
    v.published_at, v.created_at
  FROM lenses.versions v
  JOIN lenses.lenses l ON l.id = v.lens_id
  WHERE v.id = p_version_id
    AND (
      l.lenser_id = lensers.get_auth_lenser_id()
      OR (l.visibility = 'public' AND v.status = 'published')
    );
$$;


ALTER FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") IS 'Security wrapper: get a lens version. Owner sees any status; others see only published versions of public lenses.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT COALESCE(
    jsonb_agg(to_jsonb(vp.*) ORDER BY vp.label ASC),
    '[]'::jsonb
  )
  FROM lenses.version_parameters vp
  JOIN lenses.versions v   ON v.id = vp.version_id
  JOIN lenses.lenses   l   ON l.id = v.lens_id
  WHERE vp.version_id = p_version_id
    AND (
      l.lenser_id = lensers.get_auth_lenser_id()
      OR (l.visibility = 'public' AND v.status = 'published')
    );
$$;


ALTER FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") IS 'Security wrapper: return the parameter definitions for a lens version as a jsonb array ordered by label.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") RETURNS TABLE("date" "text", "count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  WITH resolved AS (
    SELECT p.id AS lenser_id,
           COALESCE(pr.hide_actions, false) AS hide_actions
    FROM   lensers.profiles p
    LEFT JOIN lensers.preferences pr ON pr.lenser_id = p.id
    WHERE  p.handle = p_handle
    LIMIT  1
  ),
  calendar AS (
    SELECT (current_date - s.i)::date AS day
    FROM generate_series(0, 364) AS s(i)
  ),
  events AS (
    -- Lens creations
    SELECT date_trunc('day', l.created_at)::date AS day
    FROM   lenses.lenses l
    JOIN   resolved r ON r.lenser_id = l.lenser_id
    WHERE  l.visibility IN ('public', 'community')
      AND  l.status = 'published'
      AND  l.created_at >= (current_date - interval '364 days')

    UNION ALL

    -- Thread creations
    SELECT date_trunc('day', t.created_at)::date AS day
    FROM   content.threads t
    JOIN   resolved r ON r.lenser_id = t.lenser_id
    WHERE  t.visibility IN ('public', 'community')
      AND  t.status = 'published'
      AND  t.created_at >= (current_date - interval '364 days')

    UNION ALL

    -- Reactions (skipped when hide_actions = true via JOIN predicate)
    SELECT date_trunc('day', rx.created_at)::date AS day
    FROM   content.reactions rx
    JOIN   resolved r ON r.lenser_id = rx.lenser_id
                      AND r.hide_actions = false
    WHERE  rx.created_at >= (current_date - interval '364 days')
  ),
  daily_counts AS (
    SELECT day, count(*) AS cnt
    FROM   events
    GROUP BY day
  )
  SELECT to_char(c.day, 'YYYY-MM-DD') AS date,
         COALESCE(dc.cnt, 0)          AS count
  FROM   calendar c
  LEFT JOIN daily_counts dc ON dc.day = c.day
  ORDER BY c.day
$$;


ALTER FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT COALESCE(jsonb_agg(to_jsonb(b.*) ORDER BY b.awarded_at DESC), '[]'::jsonb)
  FROM lensers.badges b
  WHERE b.lenser_id = p_lenser_id;
$$;


ALTER FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") IS 'Security wrapper: return all badge rows for a lenser as a jsonb array ordered by awarded_at DESC.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT to_jsonb(p.*)
  FROM lensers.profiles p
  WHERE p.id = p_lenser_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") IS 'Security wrapper: full lensers.profiles row as jsonb by lenser ID.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_language_preference"() RETURNS "text"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT pr.language
  FROM lensers.preferences pr
  WHERE pr.lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_language_preference"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_language_preference"() IS 'Security wrapper: return the preferred language from the current user''s profile preferences jsonb.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text" DEFAULT NULL::"text", "p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT p.id, p.handle, p.display_name, p.avatar_url
  FROM lensers.profiles p
  WHERE
    (p_handle    IS NOT NULL AND p.handle = p_handle)
    OR
    (p_lenser_id IS NOT NULL AND p.id = p_lenser_id)
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") IS 'Security wrapper: minimal profile (id, handle, display_name, avatar_url) by handle OR lenser_id. Pass exactly one parameter.';



CREATE TABLE IF NOT EXISTS "analytics"."lenser_stats" (
    "lenser_id" "uuid" NOT NULL,
    "thread_count" integer DEFAULT 0 NOT NULL,
    "lens_count" integer DEFAULT 0 NOT NULL,
    "follower_count" integer DEFAULT 0 NOT NULL,
    "following_count" integer DEFAULT 0 NOT NULL,
    "xp" bigint DEFAULT 0 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "mutuals_count" integer DEFAULT 0 NOT NULL,
    "badges_count" integer DEFAULT 0 NOT NULL,
    CONSTRAINT "ck_analytics_lenser_stats_badges_count_nonneg" CHECK ((("badges_count" IS NULL) OR ("badges_count" >= 0))),
    CONSTRAINT "ck_analytics_lenser_stats_follower_count_nonneg" CHECK ((("follower_count" IS NULL) OR ("follower_count" >= 0))),
    CONSTRAINT "ck_analytics_lenser_stats_following_count_nonneg" CHECK ((("following_count" IS NULL) OR ("following_count" >= 0))),
    CONSTRAINT "ck_analytics_lenser_stats_lens_count_nonneg" CHECK ((("lens_count" IS NULL) OR ("lens_count" >= 0))),
    CONSTRAINT "ck_analytics_lenser_stats_mutuals_count_nonneg" CHECK ((("mutuals_count" IS NULL) OR ("mutuals_count" >= 0))),
    CONSTRAINT "ck_analytics_lenser_stats_thread_count_nonneg" CHECK ((("thread_count" IS NULL) OR ("thread_count" >= 0))),
    CONSTRAINT "ck_analytics_lenser_stats_xp_nonneg" CHECK ((("xp" IS NULL) OR ("xp" >= 0)))
);


ALTER TABLE "analytics"."lenser_stats" OWNER TO "postgres";


CREATE OR REPLACE VIEW "lensers"."v_lenser_profile_full" AS
 SELECT "p"."id",
    "p"."handle",
    "p"."display_name",
    "p"."bio",
    "p"."headline",
    "p"."avatar_url",
    "p"."banner_url",
    "p"."location",
    "p"."website_url",
    "p"."type",
    "p"."status",
    "p"."visibility",
    "p"."created_at",
    "p"."last_active_at",
    COALESCE("s"."thread_count", 0) AS "thread_count",
    COALESCE("s"."lens_count", 0) AS "lens_count",
    COALESCE("s"."follower_count", 0) AS "follower_count",
    COALESCE("s"."following_count", 0) AS "following_count",
    COALESCE("s"."xp", (0)::bigint) AS "xp",
    COALESCE("xt"."current_level", 1) AS "current_level",
    COALESCE("xt"."total_xp", (0)::bigint) AS "total_xp",
    "rs"."score" AS "reputation_score",
    "rs"."uncertainty" AS "reputation_uncertainty",
    COALESCE("sum"("cr"."battles_played"), (0)::bigint) AS "battles_played",
    COALESCE("sum"("cr"."wins"), (0)::bigint) AS "battles_won",
    COALESCE("sum"("cr"."draws"), (0)::bigint) AS "battles_drawn",
    COALESCE("sum"("cr"."losses"), (0)::bigint) AS "battles_lost"
   FROM (((("lensers"."profiles" "p"
     LEFT JOIN "analytics"."lenser_stats" "s" ON (("s"."lenser_id" = "p"."id")))
     LEFT JOIN "xp"."totals" "xt" ON (("xt"."lenser_id" = "p"."id")))
     LEFT JOIN "reputation"."lenser_scores" "rs" ON ((("rs"."lenser_id" = "p"."id") AND ("rs"."score_type" = 'composite'::"text"))))
     LEFT JOIN "reputation"."contender_ratings" "cr" ON (("cr"."lenser_id" = "p"."id")))
  WHERE (("p"."status" <> ALL (ARRAY['deleted'::"lensers"."lenser_status", 'suspended'::"lensers"."lenser_status"])) AND ("p"."deletion_requested_at" IS NULL))
  GROUP BY "p"."id", "p"."handle", "p"."display_name", "p"."bio", "p"."headline", "p"."avatar_url", "p"."banner_url", "p"."location", "p"."website_url", "p"."type", "p"."status", "p"."visibility", "p"."created_at", "p"."last_active_at", "s"."thread_count", "s"."lens_count", "s"."follower_count", "s"."following_count", "s"."xp", "xt"."current_level", "xt"."total_xp", "rs"."score", "rs"."uncertainty";


ALTER VIEW "lensers"."v_lenser_profile_full" OWNER TO "postgres";


COMMENT ON VIEW "lensers"."v_lenser_profile_full" IS 'Full lenser profile for LenserProfilePage. Replaces 4-5 separate queries. Excludes deleted/suspended/pending-deletion profiles. battles_played/won/drawn/lost are aggregated across all battle categories.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") RETURNS "lensers"."v_lenser_profile_full"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  SELECT *
  FROM lensers.v_lenser_profile_full
  WHERE handle = lower(p_handle)
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") IS 'Returns full lenser profile data for LenserProfilePage by handle. Case-insensitive lookup via lower(). Covers stats, XP, reputation, and battle aggregates in one call.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT p.id, p.handle, p.display_name, p.avatar_url
  FROM lensers.profiles p
  WHERE p.id = ANY(p_lenser_ids);
$$;


ALTER FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) IS 'Security wrapper: batch-fetch minimal profile tuples for an array of lenser IDs.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("entity_id" "uuid", "entity_type" "text", "lenser_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT r.entity_id, r.entity_type, r.lenser_id, r.reaction, r.created_at
  FROM content.reactions r
  WHERE r.lenser_id = p_lenser_id
  ORDER BY r.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Security wrapper: paginated reaction history for a lenser, ordered by created_at DESC.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "score_type" "text", "score" numeric, "uncertainty" numeric, "computed_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT ls.id, ls.lenser_id, ls.score_type, ls.score, ls.uncertainty, ls.computed_at
  FROM reputation.lenser_scores ls
  WHERE ls.lenser_id = p_lenser_id
  ORDER BY ls.computed_at DESC;
$$;


ALTER FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") IS 'Security wrapper: return all reputation score rows for a lenser, ordered by computed_at DESC.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT COALESCE(
    jsonb_agg(to_jsonb(t.*) ORDER BY t.created_at DESC),
    '[]'::jsonb
  )
  FROM content.threads t
  WHERE t.lenser_id = p_lenser_id
    AND t.lenser_id = lensers.get_auth_lenser_id()
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Security wrapper: paginated private thread list owned by the current user. Returns empty array if p_lenser_id != caller.';



CREATE OR REPLACE FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "owner_lenser_id" "uuid", "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "content_text" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'media', 'lensers'
    AS $$
  SELECT
    o.id, o.workspace_id, o.owner_lenser_id, o.bucket, o.object_key,
    o.external_url, o.mime_type, o.media_type, o.name, o.content_text,
    o.byte_size, o.visibility, o.lifecycle_state, o.metadata,
    o.created_at, o.updated_at
  FROM media.objects o
  WHERE o.id = p_object_id
    AND (
      o.owner_lenser_id = lensers.get_auth_lenser_id()
      OR o.visibility   = 'public'
    );
$$;


ALTER FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") IS 'Security wrapper: fetch a media object row by ID. Returns the row if the caller owns it OR it is public. Callers must check lifecycle_state and visibility themselves.';



CREATE OR REPLACE FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer DEFAULT 400) RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'media', 'public'
    AS $$
DECLARE
  v_obj    RECORD;
  v_base   TEXT;
  v_url    TEXT;
BEGIN
  SELECT media_type, bucket, object_key, external_url, lifecycle_state
    INTO v_obj
    FROM media.objects
   WHERE id = p_media_id;

  IF NOT FOUND OR v_obj.lifecycle_state = 'deleted' THEN
    RETURN NULL;
  END IF;

  -- Use external_url as-is for non-storage objects
  IF v_obj.external_url IS NOT NULL THEN
    RETURN v_obj.external_url;
  END IF;

  -- Needs a storage object_key to generate CDN URL
  IF v_obj.bucket IS NULL OR v_obj.object_key IS NULL THEN
    RETURN NULL;
  END IF;

  v_base := current_setting('app.supabase_url', TRUE);
  IF v_base IS NULL OR v_base = '' THEN
    -- Fall back: return null so callers use original URL path
    RETURN NULL;
  END IF;

  -- Image: return webp transform URL
  IF v_obj.media_type = 'image' THEN
    v_url := v_base
      || '/storage/v1/object/public/'
      || v_obj.bucket
      || '/'
      || v_obj.object_key
      || '?width=' || p_width
      || '&format=webp&quality=80';
    RETURN v_url;
  END IF;

  -- Video and audio: return original CDN URL without transforms
  v_url := v_base
    || '/storage/v1/object/public/'
    || v_obj.bucket
    || '/'
    || v_obj.object_key;

  RETURN v_url;
END;
$$;


ALTER FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) IS 'CC: Returns CDN URL with ?width=<n>&format=webp&quality=80 for images; original URL for video/audio; NULL if not found or deleted.';



CREATE OR REPLACE FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "profile_id" "uuid", "ai_lenser_id" "uuid", "team_run_id" "uuid", "scope" "text", "source" "text", "content" "text", "confidence" numeric, "is_redacted" boolean, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
BEGIN
  IF p_workflow_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
    SELECT
      m.id,
      m.profile_id,
      m.ai_lenser_id,
      m.team_run_id,
      m.scope,
      m.source,
      m.content,
      m.confidence,
      m.is_redacted,
      m.created_at
    FROM agents.memories m
    JOIN agents.team_runs tr ON tr.id = m.team_run_id
    WHERE tr.workflow_id = p_workflow_id
      AND m.is_redacted = false
    ORDER BY m.created_at DESC
    LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) IS 'Phase N3: Returns memory entries written by any team_run of the given workflow_id. Honors RLS via SECURITY INVOKER. Capped at 500 rows.';



CREATE TABLE IF NOT EXISTS "battles"."model_test_runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid",
    "template_id" "uuid",
    "model_provider" "text" NOT NULL,
    "model_id" "text" NOT NULL,
    "prompt_hash" "text" NOT NULL,
    "passed" boolean NOT NULL,
    "duration_ms" integer,
    "raw_output" "jsonb",
    "violations" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "model_test_runs_duration_ms_check" CHECK ((("duration_ms" IS NULL) OR ("duration_ms" >= 0))),
    CONSTRAINT "model_test_runs_model_id_check" CHECK ((("char_length"("model_id") >= 1) AND ("char_length"("model_id") <= 128))),
    CONSTRAINT "model_test_runs_model_provider_check" CHECK ((("char_length"("model_provider") >= 1) AND ("char_length"("model_provider") <= 64))),
    CONSTRAINT "model_test_runs_prompt_hash_check" CHECK ((("char_length"("prompt_hash") >= 8) AND ("char_length"("prompt_hash") <= 128))),
    CONSTRAINT "model_test_runs_target_required" CHECK ((("battle_id" IS NOT NULL) OR ("template_id" IS NOT NULL)))
);


ALTER TABLE "battles"."model_test_runs" OWNER TO "postgres";


COMMENT ON TABLE "battles"."model_test_runs" IS 'Phase BJ append-only ledger of model conformance runs. Either battle_id or template_id must be set so each row anchors to an owned object for RLS.';



CREATE OR REPLACE FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 50) RETURNS SETOF "battles"."model_test_runs"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
  SELECT *
    FROM battles.model_test_runs
   WHERE p_battle_id IS NULL OR battle_id = p_battle_id
   ORDER BY created_at DESC
   LIMIT LEAST(GREATEST(p_limit, 1), 200)
$$;


ALTER FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 100) RETURNS TABLE("decision_id" "uuid", "occurred_at" timestamp with time zone, "decision_type" "text", "reason" "text", "is_ai_moderated" boolean, "ai_confidence" numeric, "moderator_lenser_id" "uuid", "target_entity_schema" "text", "target_entity_table" "text", "target_entity_id" "uuid", "battle_id" "uuid", "battle_title" "text", "battle_slug" "text", "is_appealed" boolean, "appeal_outcome" "text")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'audit', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller   uuid := lensers.get_auth_lenser_id();
  v_limit    int  := LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    md.id                           AS decision_id,
    md.occurred_at,
    md.decision_type,
    md.reason,
    md.is_ai_moderated,
    md.ai_confidence,
    md.moderator_lenser_id,
    md.target_entity_schema,
    md.target_entity_table,
    md.target_entity_id,
    b.id                            AS battle_id,
    b.title                         AS battle_title,
    b.slug                          AS battle_slug,
    md.is_appealed,
    md.appeal_outcome
  FROM audit.moderation_decisions md
  -- Resolve the owning battle whether the target is the battle row itself or
  -- a child entity (contender/submission/etc.) inside the same battle.
  LEFT JOIN battles.battles b ON (
    (md.target_entity_schema = 'battles'
       AND md.target_entity_table = 'battles'
       AND b.id = md.target_entity_id)
    OR
    (md.target_entity_schema = 'battles'
       AND md.target_entity_table IN ('contenders', 'submissions')
       AND b.id = (
         SELECT c.battle_id
         FROM battles.contenders c
         WHERE c.id = md.target_entity_id
       ))
  )
  WHERE b.creator_lenser_id = v_caller
    AND md.decision_type    = COALESCE(p_status, md.decision_type)
  ORDER BY md.occurred_at DESC
  LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) IS 'Phase P4: returns audit.moderation_decisions rows scoped to battles the caller created. SECURITY DEFINER; the audit table is otherwise private to service_role. Optional p_status filters by decision_type.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_api_keys"() RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "label" "text", "key_suffix" "text", "is_active" boolean, "created_at" timestamp with time zone, "revoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM ai.fn_get_my_api_keys();
END;
$$;


ALTER FUNCTION "public"."fn_get_my_api_keys"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_api_keys"() IS 'Public REST wrapper for ai.fn_get_my_api_keys. The ai schema is not exposed in PostgREST — this wrapper is the only REST-callable entry point for listing BYOK keys.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer DEFAULT 50, "p_status" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "model_id" "uuid", "provider_key" "text", "reserved_credits" numeric, "committed_credits" numeric, "status" "text", "created_at" timestamp with time zone, "held_until" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'agents', 'lensers', 'public', 'pg_catalog'
    AS $$
  SELECT r.id, r.ai_lenser_id, r.model_id, r.provider_key,
         r.reserved_credits, r.committed_credits, r.status,
         r.created_at, r.held_until
  FROM   billing.cost_reservations r
  JOIN   agents.ai_lensers al ON al.id = r.ai_lenser_id
  JOIN   lensers.profiles  p  ON p.id  = al.profile_id
  WHERE  p.user_id = auth.uid()
    AND  (p_status IS NULL OR r.status = p_status)
  ORDER  BY r.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") IS 'CG1: owner read of own cost reservations. Limit clamped to [1, 200].';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'ai', 'vault', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id    uuid;
  v_encrypted_id uuid;
  v_decrypted    text;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated: no lenser profile found';
  END IF;

  SELECT encrypted_key_id INTO v_encrypted_id
  FROM ai.keys
  WHERE id = p_key_id
    AND lenser_id = v_lenser_id
    AND is_active = true;

  IF v_encrypted_id IS NULL THEN
    RAISE EXCEPTION 'Key not found, revoked, or not owned by caller';
  END IF;

  SELECT decrypted_secret INTO v_decrypted
  FROM vault.decrypted_secrets
  WHERE id = v_encrypted_id;

  IF v_decrypted IS NULL THEN
    RAISE EXCEPTION 'Failed to decrypt key from vault';
  END IF;

  RETURN v_decrypted;
END;
$$;


ALTER FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") IS 'Decrypts a caller-owned BYOK API key from Vault. SECURITY DEFINER — runs as postgres to read vault.decrypted_secrets. Ownership enforced: lenser_id must match the authenticated caller. Only active (non-revoked) keys are returned. Works in both local and cloud Supabase environments. GUC gate removed in migration 20280116000003 — ownership check is the sufficient guard.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_lensers"() RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "type" "text", "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
  -- Human profile owned directly by this auth user
  SELECT
    p.id,
    p.handle,
    p.display_name,
    p.avatar_url,
    p.type::text,
    (pref.active_lenser_id IS NULL OR pref.active_lenser_id = p.id) AS is_active
  FROM lensers.profiles p
  LEFT JOIN lensers.preferences pref ON pref.lenser_id = p.id
  WHERE p.user_id = auth.uid()
    AND p.status  = 'active'
    AND p.type    = 'human'

  UNION ALL

  -- AI lensers whose primary owner is the auth user's human profile
  SELECT
    ai_p.id,
    ai_p.handle,
    ai_p.display_name,
    ai_p.avatar_url,
    ai_p.type::text,
    (pref.active_lenser_id = ai_p.id) AS is_active
  FROM agents.ownerships   o
  JOIN agents.ai_lensers   al    ON al.id       = o.ai_lenser_id
  JOIN lensers.profiles    ai_p  ON ai_p.id     = al.profile_id
  JOIN lensers.profiles    own_p ON own_p.id    = o.owner_lenser_id
  LEFT JOIN lensers.preferences pref ON pref.lenser_id = own_p.id
  WHERE own_p.user_id   = auth.uid()
    AND o.role          = 'owner'
    AND o.revoked_at    IS NULL
    AND ai_p.status     = 'active'

  ORDER BY is_active DESC, display_name;
$$;


ALTER FUNCTION "public"."fn_get_my_lensers"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_lensers"() IS 'Returns the authenticated user''s human lenser profile plus any AI lensers they own as primary owner, each with an is_active flag indicating the current workspace selection.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_lenses"("p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "content"."visibility_enum", "title" "text", "description" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "copy_count" integer, "like_count" integer, "saved_count" integer, "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'content', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RETURN; -- unauthenticated: empty result set
  END IF;

  RETURN QUERY
  SELECT
    l.id,
    l.lenser_id,
    l.visibility,
    COALESCE(et.title, 'Untitled') AS title,
    et.description,
    jsonb_build_object(
      'id',           prof.id,
      'handle',       prof.handle,
      'display_name', prof.display_name,
      'avatar_url',   prof.avatar_url
    ) AS author_profile,
    COALESCE(rt.reaction_totals, '{}') AS reaction_totals,
    COALESCE(rt.copy_count, 0)         AS copy_count,
    COALESCE(rt.like_count, 0)         AS like_count,
    COALESCE(rt.saved_count, 0)        AS saved_count,
    COALESCE(tg_agg.tags, '[]')        AS tags,
    l.created_at
  FROM lenses.lenses l
  LEFT JOIN content.entity_translations et
    ON et.entity_id = l.id
   AND et.entity_type = 'lens'
   AND et.is_original = true
  LEFT JOIN lensers.profiles prof ON prof.id = l.lenser_id
  LEFT JOIN LATERAL (
    SELECT
      COALESCE(jsonb_object_agg(x.reaction, x.cnt), '{}')::jsonb  AS reaction_totals,
      COALESCE(SUM(CASE WHEN x.reaction = 'copy'::content.reaction_enum  THEN x.cnt ELSE 0 END)::integer, 0) AS copy_count,
      COALESCE(SUM(CASE WHEN x.reaction = 'like'::content.reaction_enum  THEN x.cnt ELSE 0 END)::integer, 0) AS like_count,
      COALESCE(SUM(CASE WHEN x.reaction = 'saved'::content.reaction_enum THEN x.cnt ELSE 0 END)::integer, 0) AS saved_count
    FROM (
      SELECT rx.reaction, COUNT(*)::integer AS cnt
      FROM content.reactions rx
      WHERE rx.entity_type = 'lens' AND rx.entity_id = l.id
      GROUP BY rx.reaction
    ) x
  ) rt ON true
  LEFT JOIN LATERAL (
    SELECT COALESCE(jsonb_agg(
      jsonb_build_object('id', tg.id, 'slug', tg.slug, 'name', tg.slug)
    ), '[]') AS tags
    FROM content.tag_map tm
    JOIN content.tags tg ON tg.id = tm.tag_id
    WHERE tm.entity_type = 'lens' AND tm.entity_id = l.id
  ) tg_agg ON true
  WHERE l.lenser_id = v_lenser_id
  ORDER BY l.created_at DESC
  OFFSET GREATEST(p_offset, 0)
  LIMIT  LEAST(p_limit, 100);
END;
$$;


ALTER FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) IS 'Returns all lenses (public, private, unlisted) owned by the authenticated lenser. Used by the "My Lenses" filter on LensesPage.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") RETURNS TABLE("vote_value" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT v.vote_value::text
  FROM battles.votes v
  WHERE v.battle_id       = p_battle_id
    AND v.voter_lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 12, "p_visibility" "text" DEFAULT NULL::"text", "p_sort" "text" DEFAULT 'updated_at'::"text", "p_search" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "node_count" bigint, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility::text,
    w.battle_count,
    (
      SELECT count(*)
      FROM lenses.workflow_nodes wn
      WHERE wn.workflow_id = w.id
    ) AS node_count,
    w.reaction_totals,
    w.fork_count,
    w.parent_workflow_id,
    w.created_at,
    w.updated_at
  FROM lenses.workflows w
  WHERE w.lenser_id = p_lenser_id
    AND (p_visibility IS NULL OR w.visibility::text = p_visibility)
    AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%')
  ORDER BY
    CASE WHEN p_sort = 'updated_at' THEN EXTRACT(EPOCH FROM w.updated_at) END DESC,
    CASE WHEN p_sort = 'created_at' THEN EXTRACT(EPOCH FROM w.created_at) END DESC,
    CASE WHEN p_sort = 'battle_count' THEN w.battle_count::float END DESC NULLS LAST,
    w.updated_at DESC
  LIMIT p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_notification_preferences"() RETURNS TABLE("notification_type" "text", "enabled" boolean, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
BEGIN
  RETURN QUERY
    SELECT np.notification_type, np.enabled, np.updated_at
    FROM   public.notification_preferences np
    WHERE  np.lenser_id = v_lenser_id
    ORDER  BY np.notification_type;
END;
$$;


ALTER FUNCTION "public"."fn_get_notification_preferences"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_notifications"("p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "type" "text", "title" "text", "body" "text", "action_url" "text", "metadata" "jsonb", "read_at" timestamp with time zone, "created_at" timestamp with time zone, "unread_count" bigint)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_unread    BIGINT;
BEGIN
  SELECT COUNT(*) INTO v_unread
  FROM   public.notifications AS _n
  WHERE  _n.lenser_id = v_lenser_id
  AND    _n.read_at IS NULL;

  RETURN QUERY
    SELECT
      n.id, n.type, n.title, n.body, n.action_url, n.metadata, n.read_at, n.created_at,
      v_unread
    FROM   public.notifications n
    WHERE  n.lenser_id = v_lenser_id
    AND    (p_cursor IS NULL OR n.created_at < p_cursor)
    ORDER  BY n.created_at DESC
    LIMIT  p_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_pending_requests"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "source_profile_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "requested_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT
    r.id,
    r.source_profile_id,
    p.handle,
    p.display_name,
    p.avatar_url,
    r.requested_at
  FROM lensers.relationships r
  JOIN lensers.profiles p ON p.id = r.source_profile_id
  JOIN lensers.profiles me ON me.user_id = auth.uid() AND me.id = r.target_profile_id
  WHERE r.status = 'pending'
    AND p.status = 'active'
  ORDER BY r.requested_at DESC
  LIMIT LEAST(p_limit, 100)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."provider_configs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "provider_key" "text" NOT NULL,
    "ai_key_id" "uuid",
    "base_url" "text",
    "status" "text" DEFAULT 'unconfigured'::"text" NOT NULL,
    "last_checked_at" timestamp with time zone,
    "configured_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "provider_configs_status_check" CHECK (("status" = ANY (ARRAY['healthy'::"text", 'error'::"text", 'unconfigured'::"text"])))
);


ALTER TABLE "agents"."provider_configs" OWNER TO "postgres";


COMMENT ON TABLE "agents"."provider_configs" IS 'Per-agent BYOK provider configuration. Stores ai_key_id reference to ai.keys (not the key itself), status from the last health check, and optional base_url override for self-hosted providers. Raw API keys live in vault.secrets.';



COMMENT ON COLUMN "agents"."provider_configs"."ai_key_id" IS 'References ai.keys(id). NULL until the agent owner stores a key via fn_store_api_key. ON DELETE SET NULL — revoking a key marks the config as unconfigured without deleting it.';



CREATE OR REPLACE FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") RETURNS SETOF "agents"."provider_configs"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT *
  FROM   agents.provider_configs
  WHERE  ai_lenser_id = p_ai_lenser_id
    AND  agents.can_manage_ai_lenser(p_ai_lenser_id)
  ORDER BY provider_key;
$$;


ALTER FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") IS 'Returns provider configs for a given AI lenser. Authorization via can_manage_ai_lenser — returns empty set if caller is not the owner. Also used by the test-provider Edge Function for ownership validation + base_url lookup.';



CREATE OR REPLACE FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer DEFAULT 20) RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT COALESCE(jsonb_agg(to_jsonb(p.*) ORDER BY p.last_active_at DESC), '[]'::jsonb)
  FROM lensers.profiles p
  WHERE p.status = 'active'
    AND p.deletion_requested_at IS NULL
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) IS 'Security wrapper: return recently active lensers as a jsonb array, max 100.';



CREATE OR REPLACE FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) RETURNS TABLE("id" "uuid", "title" "text", "description" "text", "weight" numeric)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT rc.id, rc.title, rc.description, rc.weight
  FROM battles.rubric_criteria rc
  WHERE rc.id = ANY(p_criterion_ids);
$$;


ALTER FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer DEFAULT 30) RETURNS TABLE("rule_id" "uuid", "dispatched_count" bigint, "failed_count" bigint, "skipped_count" bigint, "queued_count" bigint, "last_attempted_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  SELECT
    ed.rule_id,
    COUNT(*) FILTER (WHERE ed.status = 'dispatched') AS dispatched_count,
    COUNT(*) FILTER (WHERE ed.status = 'failed')     AS failed_count,
    COUNT(*) FILTER (WHERE ed.status = 'skipped')    AS skipped_count,
    COUNT(*) FILTER (WHERE ed.status = 'queued')     AS queued_count,
    MAX(ed.attempted_at)                             AS last_attempted_at
  FROM automation.event_dispatches ed
  JOIN automation.trigger_rules tr ON tr.id = ed.rule_id
  WHERE tr.lenser_id = auth.uid()
    AND ed.attempted_at >= (now() - (COALESCE(p_days, 30) || ' days')::interval)
  GROUP BY ed.rule_id;
$$;


ALTER FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) IS 'Security wrapper: aggregate dispatch counts per rule for the current user over the last p_days days (default 30). Replaces client-side fan-out query.';



CREATE OR REPLACE FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "request_id" "uuid", "status" "text", "model_id" "uuid", "model_key" "text", "provider_key" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "latency_ms" integer, "token_input" integer, "token_output" integer, "credit_cost" bigint, "billing_status" "text", "error_code" "text", "error_message" "text", "artifacts" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM execution.fn_get_run_details(p_run_id);
END;
$$;


ALTER FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") IS 'Security wrapper: get execution run details including aggregated artifacts. Delegates auth enforcement to execution.fn_get_run_details.';



CREATE OR REPLACE FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "direction" "text", "source_run_id" "uuid", "source_workflow_id" "uuid", "source_node_id" "uuid", "source_output_path" "text", "target_run_id" "uuid", "target_workflow_id" "uuid", "target_node_id" "uuid", "target_input_path" "text", "transform" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
  SELECT
    p.id,
    CASE WHEN p.target_run_id = p_run_id THEN 'upstream' ELSE 'downstream' END AS direction,
    p.source_run_id,
    sr.workflow_id AS source_workflow_id,
    p.source_node_id,
    p.source_output_path,
    p.target_run_id,
    tr.workflow_id AS target_workflow_id,
    p.target_node_id,
    p.target_input_path,
    p.transform,
    p.created_at
  FROM lenses.workflow_run_provenance p
  JOIN lenses.workflow_runs sr ON sr.id = p.source_run_id
  JOIN lenses.workflow_runs tr ON tr.id = p.target_run_id
  WHERE (p.source_run_id = p_run_id OR p.target_run_id = p_run_id)
    AND EXISTS (
      SELECT 1
      FROM lenses.workflow_runs r
      JOIN lenses.workflows w ON w.id = r.workflow_id
      WHERE r.id = p_run_id
        AND (
          r.triggered_by = lensers.get_auth_lenser_id()
          OR w.visibility::text = 'public'
        )
    )
  ORDER BY p.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") IS 'Returns the provenance edges where this run is either the source or the target. Drives the n8n-style "data came from / data used by" tabs in the inspector.';



CREATE OR REPLACE FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(rr.*)
  FROM agents.run_reports rr
  WHERE rr.id = p_report_id
    AND (
      rr.ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
          AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_schedule_calendars"() RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "name" "text", "kind" "text", "dates" "date"[], "timezone" "text", "is_seed" boolean, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT sc.id, sc.lenser_id, sc.name, sc.kind, sc.dates,
         sc.timezone, sc.is_seed, sc.created_at
  FROM lenses.schedule_calendars sc
  WHERE sc.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY sc.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_get_schedule_calendars"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_schedule_calendars"() IS 'Security wrapper: list all schedule calendars owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "status" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    wr.id, wr.workflow_id, wr.schedule_id, wr.status,
    wr.started_at, wr.completed_at, wr.created_at
  FROM lenses.workflow_runs wr
  JOIN lenses.lenses l ON l.id = wr.workflow_id
  WHERE wr.workflow_id = p_workflow_id
    AND l.lenser_id = lensers.get_auth_lenser_id()
    AND (p_cursor IS NULL OR wr.created_at < p_cursor)
  ORDER BY wr.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: keyset-paginated run history for a workflow owned by the current user. Max 100 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000001'::"uuid", "p_season_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("season_id" "uuid", "season_slug" "text", "app_id" "uuid", "rank" bigint, "lenser_id" "uuid", "total_xp" bigint, "user" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
  SELECT
    v.season_id,
    v.season_slug,
    v.app_id,
    v.rank,
    v.lenser_id,
    v.total_xp,
    v."user"
  FROM public.vw_xp_leaderboard_season v
  WHERE v.app_id = p_app_id
    AND (p_season_id IS NULL OR v.season_id = p_season_id)
  ORDER BY v.rank ASC
  LIMIT p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_series"("p_series_id" "uuid") RETURNS TABLE("series_id" "uuid", "title" "text", "template_id" "uuid", "creator_lenser_id" "uuid", "round_count" integer, "current_round" integer, "status" "text", "round_number" integer, "battle_id" "uuid", "battle_slug" "text", "battle_status" "text", "winner_contender_id" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
  SELECT s.id, s.title, s.template_id, s.creator_lenser_id, s.round_count,
         s.current_round, s.status,
         sr.round_number, sr.battle_id, b.slug, b.status::text, sr.winner_contender_id
    FROM battles.series s
    LEFT JOIN battles.series_rounds sr ON sr.series_id = s.id
    LEFT JOIN battles.battles b ON b.id = sr.battle_id
   WHERE s.id = p_series_id
     AND (
       s.creator_lenser_id = auth.uid()
       OR b.status::text = 'published'
     )
   ORDER BY sr.round_number;
$$;


ALTER FUNCTION "public"."fn_get_series"("p_series_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(tm.*)
  FROM agents.team_members tm
  JOIN agents.teams t ON t.id = tm.team_id
  WHERE tm.team_id = p_team_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = t.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY tm.lane, tm.sort_order;
$$;


ALTER FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT to_jsonb(t.*)
  FROM content.threads t
  WHERE t.id = p_thread_id
    AND t.lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") IS 'Security wrapper: fetch a private/draft thread owned by the current user. Returns NULL if not found or not owned.';



CREATE OR REPLACE FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "thread_id" "uuid", "parent_reply_id" "uuid", "lenser_id" "uuid", "content" "text", "content_html" "text", "reaction_totals" "jsonb", "created_at" timestamp with time zone, "author_profile" "jsonb")
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
  WITH RECURSIVE root_page AS (
    -- Select only top-level (root) replies for this page, respecting RLS via SECURITY INVOKER
    SELECT r.id
    FROM content.thread_replies r
    WHERE r.thread_id     = p_thread_id
      AND r.parent_reply_id IS NULL
      AND r.status        = 'published'
      AND r.deleted_at    IS NULL
    ORDER BY r.created_at ASC
    LIMIT  LEAST(p_limit, 50)
    OFFSET p_offset
  ),
  descendants AS (
    -- Seed: the root replies for this page
    SELECT r.id, r.parent_reply_id
    FROM content.thread_replies r
    WHERE r.id IN (SELECT id FROM root_page)
    UNION ALL
    -- Recurse: children of already-selected replies
    SELECT r.id, r.parent_reply_id
    FROM content.thread_replies r
    INNER JOIN descendants d ON r.parent_reply_id = d.id
    WHERE r.status     = 'published'
      AND r.deleted_at IS NULL
  )
  SELECT
    v.id,
    v.thread_id,
    v.parent_reply_id,
    v.lenser_id,
    v.content,
    v.content_html,
    v.reaction_totals,
    v.created_at,
    v.author_profile
  FROM public.vw_content_thread_replies_public v
  WHERE v.id IN (SELECT id FROM descendants)
  ORDER BY v.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Paginated thread replies. Paginates by root reply count (LIMIT/OFFSET on root-level entries), then recursively fetches all descendants of the selected roots. Hard cap: 50 root replies per call. Uses SECURITY INVOKER so RLS is enforced.';



CREATE OR REPLACE FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "thread_id" "uuid", "parent_reply_id" "uuid", "lenser_id" "uuid", "content" "text", "created_at" timestamp with time zone, "deleted_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT r.id, r.thread_id, r.parent_reply_id, r.lenser_id,
         r.content, r.created_at, r.deleted_at
  FROM content.thread_replies r
  JOIN content.threads t ON t.id = r.thread_id
  WHERE r.thread_id = p_thread_id
    AND t.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY r.created_at ASC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Security wrapper: paginated replies for a private thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer DEFAULT 7) RETURNS TABLE("workflow_id" "uuid", "workflow_title" "text", "tool_name" "text", "total_invocations" bigint, "approved_count" bigint, "rejected_count" bigint, "last_invoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE
    SET "search_path" TO 'public', 'platform', 'lenses', 'agents'
    AS $$
DECLARE
  v_window int := GREATEST(LEAST(COALESCE(p_days, 7), 90), 1);
BEGIN
  IF p_ai_lenser_id IS NULL THEN
    RAISE EXCEPTION 'ai_lenser_id required' USING ERRCODE = '22023';
  END IF;

  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not_owner_of_ai_lenser' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    wr.workflow_id,
    w.title                                            AS workflow_title,
    til.tool_name,
    count(*)::bigint                                   AS total_invocations,
    count(*) FILTER (
      WHERE til.approval_status IN ('approved', 'auto_approved')
    )::bigint                                          AS approved_count,
    count(*) FILTER (
      WHERE til.approval_status = 'rejected'
    )::bigint                                          AS rejected_count,
    max(til.invoked_at)                                AS last_invoked_at
  FROM   platform.tool_invocation_logs til
  LEFT JOIN lenses.workflow_runs wr ON wr.id = til.run_id
  LEFT JOIN lenses.workflows     w  ON w.id  = wr.workflow_id
  WHERE  til.ai_lenser_id = p_ai_lenser_id
    AND  til.invoked_at >= now() - (v_window::text || ' days')::interval
  GROUP  BY wr.workflow_id, w.title, til.tool_name
  ORDER  BY total_invocations DESC, last_invoked_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) IS 'Phase Q4: per-(workflow,tool_name) rollup of platform.tool_invocation_logs over the last p_days (default 7, hard-capped to [1,90]). SECURITY INVOKER preserves underlying RLS; explicit can_manage_ai_lenser guard prevents cross-owner enumeration via the workflow JOIN.';



CREATE OR REPLACE FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") RETURNS TABLE("round_number" integer, "round_status" "text", "match_id" "uuid", "battle_id" "uuid", "battle_slug" "text", "contender_a_lenser_id" "uuid", "contender_a_handle" "text", "contender_a_avatar_url" "text", "contender_b_lenser_id" "uuid", "contender_b_handle" "text", "contender_b_avatar_url" "text", "winner_lenser_id" "uuid", "winner_handle" "text", "winner_avatar_url" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
  SELECT
    tr.round_number,
    tr.status::TEXT          AS round_status,
    tm.id                    AS match_id,
    tm.battle_id,
    b.slug                   AS battle_slug,
    tca.lenser_id            AS contender_a_lenser_id,
    pa.handle                AS contender_a_handle,
    pa.avatar_url            AS contender_a_avatar_url,
    tcb.lenser_id            AS contender_b_lenser_id,
    pb.handle                AS contender_b_handle,
    pb.avatar_url            AS contender_b_avatar_url,
    tcw.lenser_id            AS winner_lenser_id,
    pw.handle                AS winner_handle,
    pw.avatar_url            AS winner_avatar_url
  FROM battles.tournament_rounds tr
  JOIN  battles.tournament_matches tm    ON tm.round_id = tr.id
  LEFT JOIN battles.battles b            ON b.id = tm.battle_id
  LEFT JOIN battles.tournament_contenders tca ON tca.id = tm.contender_a_id
  LEFT JOIN battles.tournament_contenders tcb ON tcb.id = tm.contender_b_id
  LEFT JOIN battles.tournament_contenders tcw ON tcw.id = tm.winner_tournament_contender_id
  LEFT JOIN lensers.profiles pa ON pa.id = tca.lenser_id
  LEFT JOIN lensers.profiles pb ON pb.id = tcb.lenser_id
  LEFT JOIN lensers.profiles pw ON pw.id = tcw.lenser_id
  WHERE tr.tournament_id = p_tournament_id
  ORDER BY tr.round_number, tm.created_at;
$$;


ALTER FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT to_jsonb(t.*)
  FROM battles.tournaments t
  WHERE t.slug = p_slug
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT to_jsonb(tc.*)
  FROM battles.tournament_contenders tc
  WHERE tc.tournament_id = p_tournament_id
  ORDER BY tc.seed NULLS LAST, tc.created_at;
$$;


ALTER FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_trending_battles"("p_limit" integer DEFAULT 20, "p_cursor" numeric DEFAULT NULL::numeric) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "published_at" timestamp with time zone, "battle_type" "text", "total_vote_count" integer, "vote_velocity" numeric, "og_image_url" "text", "contender_a_name" "text", "contender_b_name" "text", "winner_slot" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    b.id, b.slug, b.title, b.status::TEXT, b.published_at,
    b.battle_type::TEXT, b.total_vote_count, b.vote_velocity, b.og_image_url,
    ca.display_name AS contender_a_name,
    cb.display_name AS contender_b_name,
    CASE
      WHEN b.winner_contender_id = ca.id THEN 'A'
      WHEN b.winner_contender_id = cb.id THEN 'B'
      ELSE NULL
    END AS winner_slot
  FROM battles.battles b
  LEFT JOIN battles.contenders ca ON ca.battle_id = b.id AND ca.slot = 'A'
  LEFT JOIN battles.contenders cb ON cb.battle_id = b.id AND cb.slot = 'B'
  WHERE b.status = 'published'
  AND   b.published_at >= now() - INTERVAL '7 days'
  AND   (p_cursor IS NULL OR b.vote_velocity < p_cursor)
  ORDER BY b.vote_velocity DESC, b.total_vote_count DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_unread_notification_count"() RETURNS bigint
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT COUNT(*)::BIGINT
  FROM   public.notifications
  WHERE  lenser_id = lensers.get_auth_lenser_id()
  AND    read_at IS NULL;
$$;


ALTER FUNCTION "public"."fn_get_unread_notification_count"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("entity_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT r.entity_id, r.reaction, r.created_at
  FROM content.reactions r
  WHERE r.entity_type::text = p_entity_type
    AND r.entity_id   = p_entity_id
    AND r.lenser_id   = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: return the current user''s reaction row for a single entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") RETURNS TABLE("version_id" "uuid", "input_contract" "jsonb", "output_contract" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_temp'
    AS $$
  select v.id, v.input_contract, v.output_contract
  from lenses.versions v
  where v.id = p_version_id
$$;


ALTER FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") IS 'Returns the input_contract + output_contract JSONB for a lens version in a single round-trip. Consumed by the workflow execution engine.';



CREATE OR REPLACE FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") RETURNS TABLE("battle_id" "uuid", "contender_id" "uuid", "raw_vote_count" integer, "weighted_vote_sum" numeric, "draw_count" integer, "rank_position" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT va.battle_id, va.contender_id, va.raw_vote_count,
         va.weighted_vote_sum, va.draw_count, va.rank_position
  FROM battles.vote_aggregates va
  WHERE va.battle_id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") RETURNS TABLE("workflow" "jsonb", "nodes" "jsonb", "edges" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers', 'content', 'pg_temp'
    AS $$
DECLARE
  v_viewer_id uuid;
BEGIN
  v_viewer_id := lensers.get_auth_lenser_id();

  RETURN QUERY
  WITH wf AS (
    SELECT * FROM public.fn_get_workflow_detail(p_workflow_id)
  ),
  viewer_reacts AS (
    SELECT
      COALESCE(jsonb_object_agg(r.reaction::text, true), '{}'::jsonb) AS reactions
    FROM content.reactions r
    WHERE v_viewer_id IS NOT NULL
      AND r.entity_type::text = 'workflow'
      AND r.entity_id          = p_workflow_id
      AND r.lenser_id          = v_viewer_id
  )
  SELECT
    to_jsonb(wf.*) || jsonb_build_object(
      'viewer_reactions', COALESCE((SELECT reactions FROM viewer_reacts), '{}'::jsonb)
    ) AS workflow,
    COALESCE(
      (SELECT jsonb_agg(n) FROM public.fn_get_workflow_nodes(p_workflow_id) n),
      '[]'::jsonb
    ) AS nodes,
    COALESCE(
      (SELECT jsonb_agg(e) FROM public.fn_get_workflow_edges(p_workflow_id) e),
      '[]'::jsonb
    ) AS edges
  FROM wf;
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") IS 'Bootstrap for WorkflowBuilderPage. Returns (workflow, nodes, edges) in one call. The workflow jsonb additionally carries viewer_reactions={reaction:true,...} so the page avoids a separate fn_get_entity_reaction_counts + fn_get_entity_reaction_status round trip.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "author_profile" "jsonb", "parent_workflow_title" "text", "parent_workflow_author_profile" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
WITH caller AS (
  SELECT lensers.get_auth_lenser_id() AS id
)
SELECT
  w.id,
  w.lenser_id,
  w.title,
  w.description,
  w.visibility::text,
  w.battle_count,
  w.reaction_totals,
  w.fork_count,
  w.parent_workflow_id,
  CASE
    WHEN prof.id IS NULL THEN NULL
    ELSE jsonb_build_object(
      'id', prof.id,
      'handle', prof.handle,
      'display_name', prof.display_name,
      'avatar_url', prof.avatar_url
    )
  END AS author_profile,
  parent.title AS parent_workflow_title,
  CASE
    WHEN parent_prof.id IS NULL THEN NULL
    ELSE jsonb_build_object(
      'id', parent_prof.id,
      'handle', parent_prof.handle,
      'display_name', parent_prof.display_name,
      'avatar_url', parent_prof.avatar_url
    )
  END AS parent_workflow_author_profile,
  w.created_at,
  w.updated_at
FROM lenses.workflows w
LEFT JOIN lensers.profiles prof
  ON prof.id = w.lenser_id
LEFT JOIN lenses.workflows parent
  ON parent.id = w.parent_workflow_id
LEFT JOIN lensers.profiles parent_prof
  ON parent_prof.id = parent.lenser_id
WHERE w.id = p_workflow_id
  AND (
    w.visibility = 'public'
    OR w.lenser_id = (SELECT id FROM caller)
  );
$$;


ALTER FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") IS 'Returns a workflow with denormalized author and parent workflow metadata for the builder page.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "source_node_id" "uuid", "target_node_id" "uuid", "source_output_key" "text", "target_param_label" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    e.id,
    e.workflow_id,
    e.source_node_id,
    e.target_node_id,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  JOIN lenses.workflows w ON w.id = e.workflow_id
  WHERE e.workflow_id = p_workflow_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
    );
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") IS 'Returns all edges for a workflow. Public workflows are readable by anyone; private/unlisted workflows are readable only by the owner.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "run_id" "uuid", "node_id" "uuid", "execution_run_id" "uuid", "status" "text", "output_data" "jsonb", "error_message" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    nr.id,
    nr.run_id,
    nr.node_id,
    nr.execution_run_id,
    nr.status,
    nr.output_data,
    nr.error_message,
    nr.started_at,
    nr.completed_at
  FROM lenses.workflow_node_results nr
  JOIN lenses.workflow_runs r ON r.id = nr.run_id
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE nr.run_id = p_run_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
      OR r.triggered_by = lensers.get_auth_lenser_id()
    );
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") IS 'Returns all node results for a workflow run. Accessible to run owner or anyone for public workflows.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "position_x" double precision, "position_y" double precision, "label" "text", "ordinal" integer, "created_at" timestamp with time zone, "config" "jsonb", "lens_visibility" "text", "lens_lenser_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    n.id,
    n.workflow_id,
    n.lens_id,
    n.version_id,
    n.position_x,
    n.position_y,
    n.label,
    n.ordinal,
    n.created_at,
    n.config,
    l.visibility::text  AS lens_visibility,
    l.lenser_id         AS lens_lenser_id
  FROM lenses.workflow_nodes n
  JOIN lenses.workflows w ON w.id = n.workflow_id
  LEFT JOIN lenses.lenses l ON l.id = n.lens_id
  WHERE n.workflow_id = p_workflow_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
    )
  ORDER BY n.ordinal, n.created_at;
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") IS 'Returns workflow nodes enriched with lens visibility and owner. Public workflows readable by anyone; private/unlisted only by owner.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "triggered_by" "uuid", "status" "text", "context_inputs" "jsonb", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    r.id,
    r.workflow_id,
    r.triggered_by,
    r.status,
    r.context_inputs,
    r.started_at,
    r.completed_at,
    r.created_at
  FROM lenses.workflow_runs r
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE r.id = p_run_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
      OR r.triggered_by = lensers.get_auth_lenser_id()
    );
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") IS 'Fetches a single workflow run record. Accessible to the run owner or anyone for public workflows.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") RETURNS TABLE("media_manifest" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT wr.media_manifest
  FROM lenses.workflow_runs wr
  WHERE wr.id = p_run_id
    AND wr.triggered_by = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") IS 'Security wrapper: get the media_manifest jsonb array for a workflow run owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "status" "text", "active_node_id" "uuid", "pending_count" integer, "waiting_count" integer, "in_flight_count" integer, "executed_count" integer, "failed_count" integer, "is_running" boolean, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "parent_run_id" "uuid", "recursion_depth" integer, "node_results" "jsonb", "upstream_count" integer, "downstream_count" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
  WITH run AS (
    SELECT *
    FROM lenses.workflow_runs r
    WHERE r.id = p_run_id
      AND EXISTS (
        SELECT 1
        FROM lenses.workflows w
        WHERE w.id = r.workflow_id
          AND (
            r.triggered_by = lensers.get_auth_lenser_id()
            OR w.visibility::text = 'public'
          )
      )
  ),
  results AS (
    SELECT
      nr.*,
      n.label    AS node_label,
      n.ordinal  AS node_ordinal
    FROM lenses.workflow_node_results nr
    LEFT JOIN lenses.workflow_nodes n ON n.id = nr.node_id
    WHERE nr.run_id = p_run_id
  ),
  agg AS (
    SELECT
      COUNT(*) FILTER (WHERE results.status = 'pending')                                                     AS pending_count,
      COUNT(*) FILTER (WHERE results.status IN ('awaiting_dependency', 'queued'))                            AS waiting_count,
      COUNT(*) FILTER (WHERE results.status IN ('running', 'streaming', 'retrying'))                         AS in_flight_count,
      COUNT(*) FILTER (WHERE results.status IN ('completed', 'skipped'))                                     AS executed_count,
      COUNT(*) FILTER (WHERE results.status IN ('failed', 'timed_out', 'blocked', 'invalidated', 'cancelled')) AS failed_count
    FROM results
  ),
  prov AS (
    SELECT
      COUNT(*) FILTER (WHERE p.target_run_id = p_run_id) AS upstream_count,
      COUNT(*) FILTER (WHERE p.source_run_id = p_run_id) AS downstream_count
    FROM lenses.workflow_run_provenance p
    WHERE p.target_run_id = p_run_id OR p.source_run_id = p_run_id
  ),
  ordered_results AS (
    SELECT jsonb_agg(
      jsonb_build_object(
        'id',              results.id,
        'node_id',         results.node_id,
        'node_label',      results.node_label,
        'node_ordinal',    results.node_ordinal,
        'status',          results.status,
        'waiting_reason',  results.waiting_reason,
        'output_data',     results.output_data,
        'error_message',   results.error_message,
        'retry_count',     results.retry_count,
        'duration_ms',     results.duration_ms,
        'ttfb_ms',         results.ttfb_ms,
        'started_at',      results.started_at,
        'completed_at',    results.completed_at
      )
      ORDER BY results.node_ordinal NULLS LAST, results.id
    ) AS data
    FROM results
  )
  SELECT
    run.id,
    run.workflow_id,
    run.status::text,
    run.active_node_id,
    COALESCE(agg.pending_count, 0)::int,
    COALESCE(agg.waiting_count, 0)::int,
    COALESCE(agg.in_flight_count, 0)::int,
    COALESCE(agg.executed_count, 0)::int,
    COALESCE(agg.failed_count, 0)::int,
    (run.status NOT IN ('completed', 'failed', 'cancelled', 'timed_out')) AS is_running,
    run.started_at,
    run.completed_at,
    run.parent_run_id,
    run.recursion_depth,
    COALESCE(ordered_results.data, '[]'::jsonb),
    COALESCE(prov.upstream_count, 0)::int,
    COALESCE(prov.downstream_count, 0)::int
  FROM run
  CROSS JOIN agg
  CROSS JOIN prov
  CROSS JOIN ordered_results;
$$;


ALTER FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") IS 'N8N-style canonical run projection. One round-trip returns active node, waiting/executed counts, ordered node results, and provenance edge counts. Drives the workflow execution inspector.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "status" "text", "scheduled_for" timestamp with time zone, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    wr.id,
    wr.workflow_id,
    wr.status,
    wr.scheduled_for,
    wr.started_at,
    wr.completed_at,
    wr.created_at
  FROM lenses.workflow_runs wr
  JOIN lenses.workflow_schedules ws ON ws.id = wr.schedule_id
  JOIN lenses.workflows w ON w.id = wr.workflow_id
  WHERE wr.schedule_id = p_schedule_id
    AND wr.scheduled_for IS NOT NULL
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wr.scheduled_for DESC
  LIMIT 50;
$$;


ALTER FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") IS 'Returns the 50 most recent scheduled workflow_runs for a given schedule, newest first.
   Accessible only to the workflow owner via lensers.get_auth_lenser_id().';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "workflow_title" "text", "cron_expr" "text", "timezone" "text", "global_model_id" "text", "inputs_template" "jsonb", "is_active" boolean, "assignee_type" "text", "assignee_id" "uuid", "workflow_assignment_id" "uuid", "approval_policy" "jsonb", "retry_policy" "jsonb", "failure_policy" "jsonb", "queue_policy" "jsonb", "next_run_at" timestamp with time zone, "last_run_at" timestamp with time zone, "last_run_id" "uuid", "last_dispatch_status" "text", "last_error_at" timestamp with time zone, "last_error_message" "text", "last_completed_at" timestamp with time zone, "last_result" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    s.id,
    s.workflow_id,
    w.title AS workflow_title,
    s.cron_expr,
    s.timezone,
    s.global_model_id,
    s.inputs_template,
    s.is_active,
    s.assignee_type,
    s.assignee_id,
    s.workflow_assignment_id,
    s.approval_policy,
    s.retry_policy,
    s.failure_policy,
    s.queue_policy,
    s.next_run_at,
    s.last_run_at,
    s.last_run_id,
    s.last_dispatch_status,
    s.last_error_at,
    s.last_error_message,
    s.last_completed_at,
    s.last_result,
    s.created_at
  FROM lenses.workflow_schedules s
  JOIN lenses.workflows w ON w.id = s.workflow_id
  WHERE w.lenser_id = lensers.get_auth_lenser_id()
    AND (p_workflow_id IS NULL OR s.workflow_id = p_workflow_id)
  ORDER BY s.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "version_number" integer, "changelog" "text", "status" "text", "published_at" timestamp with time zone, "created_by" "uuid", "created_at" timestamp with time zone, "node_count" bigint, "edge_count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT * FROM lenses.fn_get_workflow_versions(p_workflow_id);
$$;


ALTER FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") IS 'Public wrapper: returns versioned snapshots for a workflow the caller can access. Delegates to lenses.fn_get_workflow_versions which enforces ownership/visibility via RLS.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50) RETURNS TABLE("run_id" "uuid", "run_type" "text", "ai_lenser_id" "uuid", "status" "text", "approval_status" "text", "total_cost" numeric, "step_count" bigint, "memory_write_count" bigint, "latest_evaluation_score" numeric, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "duration_seconds" numeric)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lenses', 'lensers', 'auth'
    AS $$
DECLARE
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
BEGIN
  IF p_ai_lenser_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT
    v.run_id,
    v.run_type,
    v.ai_lenser_id,
    v.status,
    v.approval_status,
    v.total_cost,
    v.step_count,
    v.memory_write_count,
    v.latest_evaluation_score,
    v.started_at,
    v.completed_at,
    v.duration_seconds
  FROM agents.v_run_unified v
  WHERE v.ai_lenser_id = p_ai_lenser_id
  ORDER BY v.started_at DESC NULLS LAST
  LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) IS 'Public SECURITY DEFINER wrapper around agents.v_run_unified. Returns up to p_limit (max 200, default 50) unified runs for the given AI agent. RLS enforcement is via agents.can_manage_ai_lenser inside the view. Returns empty set instead of 404/500 for unknown or inaccessible agents.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ws.*)
  FROM agents.workspace_settings ws
  WHERE ws.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_gate_kind" "text" DEFAULT NULL::"text", "p_hours" integer DEFAULT 24) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_caller     uuid := lensers.get_auth_lenser_id();
  v_id         uuid;
  v_expires    timestamptz;
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not_owner_of_ai_lenser' USING ERRCODE = '42501';
  END IF;

  IF p_workflow_id IS NULL AND (p_gate_kind IS NULL OR length(trim(p_gate_kind)) = 0) THEN
    RAISE EXCEPTION 'standing approval requires workflow_id or gate_kind' USING ERRCODE = '22023';
  END IF;

  v_expires := CASE
    WHEN p_hours IS NULL OR p_hours <= 0 THEN NULL
    ELSE now() + (p_hours::text || ' hours')::interval
  END;

  INSERT INTO agents.standing_approvals (
    ai_lenser_id, workflow_id, gate_kind, expires_at, created_by
  ) VALUES (
    p_ai_lenser_id, p_workflow_id, NULLIF(trim(p_gate_kind), ''), v_expires, v_caller
  )
  RETURNING id INTO v_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id, action_type, result, metadata
  ) VALUES (
    p_ai_lenser_id,
    'standing_approval_granted',
    'success',
    jsonb_build_object(
      'standing_approval_id', v_id,
      'workflow_id',          p_workflow_id,
      'gate_kind',            NULLIF(trim(p_gate_kind), ''),
      'expires_at',            v_expires,
      'granted_by',            v_caller
    )
  );

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) IS 'Phase Q2: grants a standing approval for an AI lenser. p_hours<=0 or NULL means no expiry. Returns the standing_approvals.id. Owner-only via agents.can_manage_ai_lenser; writes an action_logs entry.';



CREATE OR REPLACE FUNCTION "public"."fn_health"() RETURNS integer
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT 1;
$$;


ALTER FUNCTION "public"."fn_health"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_health"() IS 'Phase K4 health probe. Returns 1. Called by the platform-api GET /health route to verify database reachability.';



CREATE OR REPLACE FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  UPDATE lenses.workflow_runs
  SET heartbeat_at  = now(),
      run_worker_id = COALESCE(run_worker_id, p_worker_id)
  WHERE id = p_run_id
    AND status IN ('queued', 'running', 'streaming', 'recovered');
END;
$$;


ALTER FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") IS 'Lightweight keep-alive pinged by the execution worker every HEARTBEAT_MS. Used by fn_claim_stale_workflow_run (Phase 7) to detect stalled runs. Only writes while the run is in a non-terminal state.';



CREATE TABLE IF NOT EXISTS "agents"."agent_run_events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "team_run_id" "uuid" NOT NULL,
    "agent_run_step_id" "uuid",
    "event_type" "text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."agent_run_events" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."ai_lensers" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "profile_id" "uuid" NOT NULL,
    "runtime_pref" "text" DEFAULT 'cloud'::"text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "suspended_at" timestamp with time zone,
    "suspended_reason" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "personality_note" "text",
    "archived_at" timestamp with time zone,
    "deleted_at" timestamp with time zone,
    CONSTRAINT "ai_lensers_runtime_check" CHECK (("runtime_pref" = ANY (ARRAY['cloud'::"text", 'local'::"text", 'hybrid'::"text"])))
);


ALTER TABLE "agents"."ai_lensers" OWNER TO "postgres";


COMMENT ON TABLE "agents"."ai_lensers" IS 'Extension record for AI Lenser profiles. 1:1 with lensers.profiles (type=''ai''). Holds runtime state only. All autonomous behavior policies live in agents.policies. Created atomically via agents.fn_create_ai_lenser.';



COMMENT ON COLUMN "agents"."ai_lensers"."personality_note" IS 'Free-text role/tone/behavior description authored by the agent owner. Injected as meta-context alongside the default lens system prompt at agent runtime.';



CREATE TABLE IF NOT EXISTS "agents"."ownerships" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "owner_lenser_id" "uuid" NOT NULL,
    "role" "text" DEFAULT 'owner'::"text" NOT NULL,
    "permission_scope" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "granted_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "revoked_at" timestamp with time zone,
    CONSTRAINT "ownerships_role_check" CHECK (("role" = ANY (ARRAY['owner'::"text", 'co_owner'::"text", 'operator'::"text"])))
);


ALTER TABLE "agents"."ownerships" OWNER TO "postgres";


COMMENT ON TABLE "agents"."ownerships" IS 'Owner/operator delegation from a human lenser to an AI Lenser. A partial unique index enforces exactly one active primary owner. co_owner and operator roles may be granted freely.';



COMMENT ON COLUMN "agents"."ownerships"."permission_scope" IS 'Array of permitted actions, e.g. ''{join_battles, vote, create_battles, spend_credits}''. Empty array = no delegation beyond existence.';



CREATE TABLE IF NOT EXISTS "agents"."team_runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "team_id" "uuid",
    "workflow_id" "uuid",
    "workflow_run_id" "uuid",
    "workflow_assignment_id" "uuid",
    "status" "text" DEFAULT 'queued'::"text" NOT NULL,
    "approval_status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "scratchpad" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "shared_scratchpad" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "shared_scratchpad_version" integer DEFAULT 0 NOT NULL,
    "parent_team_run_id" "uuid",
    "recursion_depth" integer DEFAULT 0 NOT NULL,
    "redacted_agent_snapshot" "jsonb",
    "redacted_agent_snapshot_hash" "text",
    CONSTRAINT "team_runs_approval_status_check" CHECK (("approval_status" = ANY (ARRAY['pending'::"text", 'approved'::"text", 'rejected'::"text", 'not_required'::"text", 'timed_out'::"text"]))),
    CONSTRAINT "team_runs_recursion_depth_range" CHECK ((("recursion_depth" >= 0) AND ("recursion_depth" <= 8))),
    CONSTRAINT "team_runs_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'running'::"text", 'completed'::"text", 'failed'::"text", 'cancelled'::"text", 'blocked'::"text"])))
);


ALTER TABLE "agents"."team_runs" OWNER TO "postgres";


COMMENT ON COLUMN "agents"."team_runs"."shared_scratchpad" IS 'Phase X3: shared multi-agent JSON scratchpad. Mutated only via agents.fn_merge_shared_scratchpad with optimistic locking.';



COMMENT ON COLUMN "agents"."team_runs"."shared_scratchpad_version" IS 'Phase X3: monotonic version counter for optimistic locking. Starts at 0.';



COMMENT ON COLUMN "agents"."team_runs"."parent_team_run_id" IS 'D5: parent team_run that delegated to this run; used to enforce recursion cap.';



COMMENT ON COLUMN "agents"."team_runs"."recursion_depth" IS 'D5: cached depth in parent_team_run_id chain; rejected at start when > 8.';



CREATE OR REPLACE VIEW "agents"."v_human_fleet_logs" AS
 SELECT "o"."owner_lenser_id" AS "human_lenser_id",
    "ev"."id" AS "event_id",
    "ev"."team_run_id",
    "tr"."ai_lenser_id",
    "p"."handle" AS "agent_handle",
    "ev"."event_type",
    "ev"."payload",
    "ev"."occurred_at"
   FROM (((("agents"."ownerships" "o"
     JOIN "agents"."ai_lensers" "al" ON (("al"."id" = "o"."ai_lenser_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "al"."profile_id")))
     JOIN "agents"."team_runs" "tr" ON (("tr"."ai_lenser_id" = "al"."id")))
     JOIN "agents"."agent_run_events" "ev" ON (("ev"."team_run_id" = "tr"."id")));


ALTER VIEW "agents"."v_human_fleet_logs" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid" DEFAULT NULL::"uuid", "p_event_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 100, "p_offset" integer DEFAULT 0) RETURNS SETOF "agents"."v_human_fleet_logs"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT *
    FROM agents.v_human_fleet_logs
   WHERE human_lenser_id = p_human_lenser_id
     AND human_lenser_id = auth.uid()
     AND (p_run_id     IS NULL OR team_run_id = p_run_id)
     AND (p_event_type IS NULL OR event_type   = p_event_type)
   ORDER BY occurred_at DESC
   LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 100), 500))
  OFFSET GREATEST(0, COALESCE(p_offset, 0));
$$;


ALTER FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE VIEW "agents"."v_human_fleet_runs" AS
 SELECT "o"."owner_lenser_id" AS "human_lenser_id",
    "tr"."id" AS "run_id",
    "tr"."ai_lenser_id",
    "p"."handle" AS "agent_handle",
    "tr"."team_id",
    "tr"."workflow_id",
    "tr"."status",
    "tr"."approval_status",
    "tr"."metadata",
    "tr"."created_at",
    "tr"."started_at",
    "tr"."completed_at"
   FROM ((("agents"."ownerships" "o"
     JOIN "agents"."ai_lensers" "al" ON (("al"."id" = "o"."ai_lenser_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "al"."profile_id")))
     JOIN "agents"."team_runs" "tr" ON (("tr"."ai_lenser_id" = "al"."id")));


ALTER VIEW "agents"."v_human_fleet_runs" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text" DEFAULT NULL::"text", "p_agent_id" "uuid" DEFAULT NULL::"uuid", "p_since" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS SETOF "agents"."v_human_fleet_runs"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT *
    FROM agents.v_human_fleet_runs
   WHERE human_lenser_id = p_human_lenser_id
     AND human_lenser_id = auth.uid()
     AND (p_status   IS NULL OR status     = p_status)
     AND (p_agent_id IS NULL OR ai_lenser_id = p_agent_id)
     AND (p_since    IS NULL OR created_at >= p_since)
   ORDER BY created_at DESC
   LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 50), 200))
  OFFSET GREATEST(0, COALESCE(p_offset, 0));
$$;


ALTER FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text" DEFAULT NULL::"text", "p_action_url" "text" DEFAULT NULL::"text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb", "p_actor_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_id UUID;
BEGIN
  -- Recipient must exist
  IF NOT EXISTS (SELECT 1 FROM lensers.profiles WHERE id = p_lenser_id) THEN
    RETURN NULL;
  END IF;

  -- Privacy gate: only runs when an actor is identified
  IF p_actor_id IS NOT NULL THEN
    IF NOT public.fn_should_send_notification(p_lenser_id, p_actor_id, p_type) THEN
      RETURN NULL;
    END IF;
  END IF;

  INSERT INTO public.notifications (lenser_id, type, title, body, action_url, metadata)
  VALUES (p_lenser_id, p_type, p_title, p_body, p_action_url, p_metadata)
  RETURNING id INTO v_id;

  RETURN v_id;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_insert_notification failed for lenser=% type=%: %',
    p_lenser_id, p_type, SQLERRM;
  RETURN NULL;
END;
$$;


ALTER FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_integrity_probe_elo_log"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation', 'pg_catalog'
    AS $$
DECLARE
  v_table_exists boolean;
  v_has_pk_on_battle_id boolean;
BEGIN
  SELECT EXISTS (
    SELECT 1 FROM pg_class c
    JOIN pg_namespace n ON n.oid = c.relnamespace
    WHERE n.nspname = 'reputation' AND c.relname = 'elo_battle_log'
  ) INTO v_table_exists;

  SELECT EXISTS (
    SELECT 1 FROM pg_constraint con
    JOIN pg_class c ON c.oid = con.conrelid
    JOIN pg_namespace n ON n.oid = c.relnamespace
    JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY (con.conkey)
    WHERE n.nspname = 'reputation'
      AND c.relname = 'elo_battle_log'
      AND con.contype = 'p'
      AND a.attname = 'battle_id'
  ) INTO v_has_pk_on_battle_id;

  RETURN jsonb_build_object(
    'table_exists', v_table_exists,
    'has_pk_on_battle_id', v_has_pk_on_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_integrity_probe_elo_log"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_integrity_probe_elo_log"() IS 'Phase P automation. Reflects the existence + PK shape of reputation.elo_battle_log so the staging integrity script can verify idempotency infrastructure. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'audit'
    AS $$
DECLARE
  v_decision_id uuid;
  v_audit_row_present boolean;
BEGIN
  -- Insert a synthetic flagged decision (AI-moderated, no human moderator).
  INSERT INTO audit.moderation_decisions
    (target_entity_schema, target_entity_table, target_entity_id,
     decision_type, reason, is_ai_moderated, ai_confidence)
  VALUES
    ('battles', 'battles', p_target_entity_id,
     'flagged', '__integrity_probe_synthetic_flag', true, 0.95)
  RETURNING id INTO v_decision_id;

  -- Verify the row landed.
  SELECT EXISTS (
    SELECT 1 FROM audit.moderation_decisions
    WHERE id = v_decision_id AND decision_type = 'flagged'
  ) INTO v_audit_row_present;

  -- Cleanup so probe rows don't accumulate.
  DELETE FROM audit.moderation_decisions WHERE id = v_decision_id;

  RETURN jsonb_build_object(
    'decision_id', v_decision_id,
    'audit_row_present', v_audit_row_present,
    'cleaned_up', true
  );
END;
$$;


ALTER FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") IS 'Phase P automation. Inserts a synthetic flagged moderation_decisions row, verifies it landed, then cleans up. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'audit'
    AS $$
DECLARE
  v_probe_id text := '__integrity_probe_' || gen_random_uuid()::text;
  v_decision_id uuid;
  v_outbox_id uuid;
  v_prior_url text := current_setting('app.moderation_webhook_url', true);
BEGIN
  -- Temporarily point the webhook URL at the listener.
  PERFORM set_config('app.moderation_webhook_url', p_target_url, true);

  -- Insert a flagged decision; the AFTER INSERT trigger enqueues into outbox.
  INSERT INTO audit.moderation_decisions
    (target_entity_schema, target_entity_table, target_entity_id,
     decision_type, reason, is_ai_moderated, ai_confidence)
  VALUES
    ('battles', 'battles', gen_random_uuid(),
     'flagged', v_probe_id, true, 0.99)
  RETURNING id INTO v_decision_id;

  -- Restore prior URL setting for the rest of the session.
  PERFORM set_config('app.moderation_webhook_url', COALESCE(v_prior_url, ''), true);

  -- Force-run the dispatcher so the listener gets the POST without waiting
  -- for the cron tick. dispatch_webhook_outbox is idempotent.
  PERFORM audit.fn_dispatch_webhook_outbox(50);

  -- Cleanup the synthetic decision.
  DELETE FROM audit.moderation_decisions WHERE id = v_decision_id;

  RETURN jsonb_build_object(
    'probe_id', v_probe_id,
    'decision_id', v_decision_id,
    'enqueued', true
  );
END;
$$;


ALTER FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") IS 'Phase P automation. Inserts a synthetic flagged decision with the listener URL set transiently, force-dispatches the outbox, cleans up. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid" DEFAULT NULL::"uuid", "p_display_name" "text" DEFAULT NULL::"text", "p_handle" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "contender_type" "text", "display_name" "text", "contender_ref_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_battle    battles.battles;
  v_profile   lensers.profiles;
  v_ref_id    uuid;
  v_display   text;
BEGIN
  -- Verify caller owns the battle
  SELECT b.* INTO v_battle
    FROM battles.battles b
   WHERE b.id = p_battle_id;

  IF NOT FOUND OR v_battle.creator_lenser_id <> v_lenser_id THEN
    RAISE EXCEPTION 'invite_contender_forbidden' USING ERRCODE = '42501';
  END IF;

  -- Resolve contender: handle takes priority; fall back to ref_id
  IF p_handle IS NOT NULL THEN
    SELECT p.* INTO v_profile
      FROM lensers.profiles p
     WHERE lower(p.handle) = lower(ltrim(p_handle, '@'))
       AND p.status = 'active'
       AND p.deletion_requested_at IS NULL;

    IF NOT FOUND THEN
      RAISE EXCEPTION 'lenser_not_found' USING ERRCODE = 'P0002';
    END IF;

    v_ref_id  := v_profile.id;
    v_display := COALESCE(p_display_name, v_profile.display_name);

  ELSIF p_contender_ref_id IS NOT NULL THEN
    v_ref_id  := p_contender_ref_id;
    v_display := p_display_name;

    SELECT p.* INTO v_profile
      FROM lensers.profiles p
     WHERE p.id = p_contender_ref_id;

  ELSE
    RAISE EXCEPTION 'must provide p_contender_ref_id or p_handle' USING ERRCODE = '22023';
  END IF;

  -- Validate lenser type against battle mode
  IF v_profile.id IS NOT NULL THEN
    CASE v_battle.battle_type
      WHEN 'ai_vs_ai' THEN
        IF v_profile.type <> 'ai' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: this battle requires an AI lenser'
            USING ERRCODE = '22000';
        END IF;

      WHEN 'human_vs_human_ai_votes', 'human_vs_human_open_votes' THEN
        IF v_profile.type <> 'human' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: this battle requires a human lenser'
            USING ERRCODE = '22000';
        END IF;

      WHEN 'human_vs_ai' THEN
        IF p_slot = 'A' AND v_profile.type <> 'human' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: slot A in a human_vs_ai battle must be a human lenser'
            USING ERRCODE = '22000';
        ELSIF p_slot = 'B' AND v_profile.type <> 'ai' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: slot B in a human_vs_ai battle must be an AI lenser'
            USING ERRCODE = '22000';
        END IF;

      ELSE
        -- lenser_battle, workflow_battle: any lenser type is allowed
        NULL;
    END CASE;
  END IF;

  -- Insert via CTE to avoid the ambiguous "id" reference between the
  -- RETURNS TABLE output column and the actual contenders.id table column.
  RETURN QUERY
  WITH ins AS (
    INSERT INTO battles.contenders (
      battle_id, slot, contender_type, contender_ref_id, display_name,
      entry_mode, contender_status
    ) VALUES (
      p_battle_id,
      p_slot,
      p_contender_type::battles.contender_type_enum,
      v_ref_id,
      v_display,
      'invited',
      'pending'
    )
    RETURNING *
  )
  SELECT
    ins.id,
    ins.battle_id,
    ins.slot::text,
    ins.contender_type::text,
    ins.display_name,
    ins.contender_ref_id
  FROM ins;
END;
$$;


ALTER FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") IS 'Invite a lenser as a contender. Caller must own the battle. Accepts p_contender_ref_id (UUID) or p_handle (text, @-prefix stripped) for lookup. Validates lenser.type against battle_type: ai_vs_ai requires ai, human_vs_human_* requires human, human_vs_ai enforces human on slot A and ai on slot B. Ambiguous id column fixed via CTE pattern.';



CREATE OR REPLACE FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_tool          agents.tools_registry;
  v_assignment    agents.tool_assignments;
  v_profile       agents.tool_profiles;
  v_requires      boolean := false;
  v_appr_status   text;
  v_status        text;
  v_started_at    timestamptz;
  v_id            uuid;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_tool FROM agents.tools_registry WHERE id = p_tool_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'tool not found' USING ERRCODE = 'P0002';
  END IF;
  IF v_tool.status <> 'active' THEN
    RAISE EXCEPTION 'tool % is %', v_tool.key, v_tool.status USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_assignment
    FROM agents.tool_assignments
   WHERE ai_lenser_id = p_ai_lenser_id AND tool_id = p_tool_id;
  IF NOT FOUND OR v_assignment.allowed IS DISTINCT FROM true THEN
    RAISE EXCEPTION 'tool % not assigned to agent', v_tool.key USING ERRCODE = '42501';
  END IF;

  IF v_assignment.profile_id IS NOT NULL THEN
    SELECT * INTO v_profile FROM agents.tool_profiles WHERE id = v_assignment.profile_id;
    IF FOUND AND v_profile.requires_approval THEN
      v_requires := true;
    END IF;
  END IF;

  IF v_tool.requires_approval OR v_tool.egress_class = 'write' THEN
    v_requires := true;
  END IF;

  IF v_requires THEN
    v_appr_status := 'pending';
    v_status      := 'pending';
    v_started_at  := NULL;
  ELSE
    v_appr_status := 'not_required';
    v_status      := 'running';
    v_started_at  := now();
  END IF;

  INSERT INTO agents.tool_invocations (
    team_run_id, agent_run_step_id, tool_id, ai_lenser_id,
    input, status, approval_status, approval_required, started_at
  )
  VALUES (
    p_team_run_id, p_agent_run_step_id, p_tool_id, p_ai_lenser_id,
    COALESCE(p_input, '{}'::jsonb), v_status, v_appr_status, v_requires, v_started_at
  )
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1
    FROM   lensers.relationships
    WHERE  source_profile_id = p_recipient_id
      AND  target_profile_id = p_actor_id
      AND  status = 'blocked'
      AND  removed_at IS NULL
  );
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_is_notification_blocked failed: %', SQLERRM;
  RETURN false;  -- fail-open: never suppress by accident
END;
$$;


ALTER FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  RETURN COALESCE(
    (SELECT NOT enabled
     FROM   public.notification_preferences
     WHERE  lenser_id         = p_lenser_id
       AND  notification_type = p_type),
    false
  );
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_is_notification_muted failed: %', SQLERRM;
  RETURN false;  -- fail-open
END;
$$;


ALTER FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_is_super_admin"() RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  SELECT COALESCE(
    (SELECT is_super_admin FROM lensers.profiles WHERE id = auth.uid() LIMIT 1),
    FALSE
  );
$$;


ALTER FUNCTION "public"."fn_is_super_admin"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_journey_state_get"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_state JSONB;
BEGIN
  SELECT journey_state
    INTO v_state
    FROM lensers.profiles
   WHERE user_id = auth.uid();

  RETURN COALESCE(v_state, '{}'::JSONB);
END;
$$;


ALTER FUNCTION "public"."fn_journey_state_get"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_journey_state_get"() IS 'Returns the journey_state JSONB for the calling lenser. Safe to call before any step is marked.';



CREATE OR REPLACE FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean DEFAULT true) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  UPDATE lensers.profiles
     SET journey_state = journey_state || jsonb_build_object(p_step, p_done),
         updated_at    = NOW()
   WHERE user_id = auth.uid();

  -- Silently no-op if the profile row doesn't exist yet; the CLI handles null.
  IF NOT FOUND THEN
    NULL;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) IS 'Sets or clears a journey step key for the calling lenser. Idempotent; no-ops on missing profile.';



CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid" DEFAULT NULL::"uuid", "p_reason" "text" DEFAULT 'emergency stop'::"text", "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_operator_id UUID;
  v_switch_id   UUID;
BEGIN
  -- Admin-only
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'kill_switch_activate_forbidden: admin role required';
  END IF;

  SELECT id INTO v_operator_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  IF v_operator_id IS NULL THEN
    RAISE EXCEPTION 'kill_switch_activate_no_profile: lenser profile not found';
  END IF;

  INSERT INTO admin.kill_switches (scope, target_id, operator_id, reason, expires_at)
  VALUES (p_scope, p_target_id, v_operator_id, p_reason, p_expires_at)
  RETURNING id INTO v_switch_id;

  -- Audit trail
  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'kill_switch.activated',
    'lenser',
    v_operator_id,
    'warn',
    jsonb_build_object(
      'switch_id', v_switch_id,
      'scope',     p_scope,
      'target_id', p_target_id,
      'reason',    p_reason
    )
  );

  RETURN v_switch_id;
END;
$$;


ALTER FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid" DEFAULT NULL::"uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'admin', 'public'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM   admin.kill_switches ks
    WHERE  ks.lifted_at IS NULL
    AND    (ks.expires_at IS NULL OR ks.expires_at > now())
    AND    (
             -- System-wide switch halts everything
             ks.scope = 'system'
             OR
             -- Scope + target match
             (ks.scope = p_scope AND ks.target_id = p_target_id)
           )
  );
$$;


ALTER FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_operator_id UUID;
  v_switch      admin.kill_switches%ROWTYPE;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'kill_switch_lift_forbidden: admin role required';
  END IF;

  SELECT * INTO v_switch FROM admin.kill_switches WHERE id = p_switch_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'kill_switch_not_found: %', p_switch_id;
  END IF;

  IF v_switch.lifted_at IS NOT NULL THEN
    RAISE EXCEPTION 'kill_switch_already_lifted: %', p_switch_id;
  END IF;

  UPDATE admin.kill_switches SET lifted_at = now() WHERE id = p_switch_id;

  SELECT id INTO v_operator_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'kill_switch.lifted',
    'lenser',
    v_operator_id,
    'info',
    jsonb_build_object(
      'switch_id',     p_switch_id,
      'original_scope', v_switch.scope,
      'target_id',     v_switch.target_id
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_list"() RETURNS TABLE("id" "uuid", "scope" "text", "target_id" "uuid", "reason" "text", "expires_at" timestamp with time zone, "activated_at" timestamp with time zone, "lifted_at" timestamp with time zone, "operator_handle" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
  SELECT
    ks.id,
    ks.scope,
    ks.target_id,
    ks.reason,
    ks.expires_at,
    ks.activated_at,
    ks.lifted_at,
    p.handle AS operator_handle
  FROM  admin.kill_switches ks
  LEFT  JOIN lensers.profiles p ON p.id = ks.operator_id
  ORDER BY ks.activated_at DESC;
$$;


ALTER FUNCTION "public"."fn_kill_switch_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean DEFAULT false) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lenses'
    AS $$
DECLARE
  v_lens_id UUID;
BEGIN
  BEGIN
    v_lens_id := p_slug::uuid;
  EXCEPTION WHEN invalid_text_representation THEN
    SELECT l.id INTO v_lens_id
    FROM lenses.lenses l
    LEFT JOIN content.entity_translations et
      ON et.entity_id = l.id
     AND et.entity_type = 'lens'::content.entity_type_enum
     AND et.is_original = TRUE
    WHERE lower(replace(COALESCE(et.title, l.id::text), ' ', '-')) = lower(p_slug)
       OR lower(COALESCE(et.title, '')) = lower(p_slug)
    ORDER BY l.created_at DESC
    LIMIT 1;
  END;

  IF v_lens_id IS NULL THEN
    RAISE EXCEPTION 'lens_not_found: %', p_slug USING ERRCODE = 'P0002';
  END IF;

  RETURN public.fn_artifact_pin('lens', v_lens_id, NOT COALESCE(p_unstar, FALSE));
END;
$$;


ALTER FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text" DEFAULT ''::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_row lensers.profiles;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  INSERT INTO lensers.profiles (user_id, handle, display_name, bio, headline, onboarding_step)
  VALUES (v_uid, p_handle, p_display_name, COALESCE(p_bio, ''), NULL, 1)
  RETURNING * INTO v_row;

  RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  v_result := public.fn_request_follow(p_following_id);
  -- Return old format for backward compat
  RETURN jsonb_build_object('following', true);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_active_profile"() RETURNS SETOF "lensers"."profiles"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT p.*
  FROM lensers.profiles p
  WHERE p.id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_lensers_get_active_profile"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_get_active_profile"() IS 'Returns the active lenser workspace profile for the authenticated user. Unlike user_id-based lookups this follows preferences.active_lenser_id when an owned AI workspace is selected.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_current_id"() RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT lp.id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
    AND lp.status = 'active'
    AND lp.deletion_requested_at IS NULL
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_lensers_get_current_id"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text" DEFAULT 'following'::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "is_following" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT
    lp.id,
    lp.handle,
    lp.display_name,
    lp.avatar_url,
    EXISTS (
      SELECT 1 FROM lensers.relationships r2
      JOIN lensers.profiles me ON me.user_id = auth.uid()
      WHERE r2.source_profile_id = me.id
        AND r2.target_profile_id = lp.id
        AND r2.status = 'accepted'
    ) AS is_following
  FROM lensers.relationships r
  JOIN lensers.profiles lp ON lp.id = CASE
    WHEN p_type = 'followers' THEN r.source_profile_id
    ELSE r.target_profile_id
  END
  WHERE CASE
    WHEN p_type = 'followers' THEN r.target_profile_id = p_lenser_id
    ELSE r.source_profile_id = p_lenser_id
  END
    AND r.status = 'accepted'
    AND lp.status = 'active'::lensers.lenser_status
    AND lp.deletion_requested_at IS NULL
  ORDER BY r.accepted_at DESC
  LIMIT LEAST(p_limit, 100)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_fresh_profile"() RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "headline" "text", "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  select 
    id,
    handle,
    display_name,
    avatar_url,
    headline,
    updated_at
  from lensers.profiles
  where user_id = auth.uid();
$$;


ALTER FUNCTION "public"."fn_lensers_get_fresh_profile"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  select p.is_in_waiting_list
  from lensers.profiles p
  where p.user_id = auth.uid()
  limit 1;
$$;


ALTER FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text" DEFAULT 'all_time'::"text", "p_limit" integer DEFAULT 20) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_xp" bigint, "current_level" integer, "lenser_score" double precision, "rank" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'xp', 'lenses', 'content'
    AS $$
WITH ranked AS (
  SELECT
    lp.id            AS lenser_id,
    lp.handle,
    lp.display_name,
    lp.avatar_url,
    COALESCE(xt.total_xp,      0)::bigint AS total_xp,
    COALESCE(xt.current_level, 1)          AS current_level,
    ROW_NUMBER() OVER (ORDER BY COALESCE(xt.total_xp, 0) DESC) AS rank
  FROM lensers.profiles lp
  LEFT JOIN (
    SELECT t.lenser_id,
           SUM(t.total_xp)       AS total_xp,
           MAX(t.current_level)  AS current_level
    FROM xp.totals t
    GROUP BY t.lenser_id
  ) xt ON xt.lenser_id = lp.id
  WHERE lp.status              = 'active'
    AND lp.visibility          = 'public'
    AND lp.deletion_requested_at IS NULL
    AND CASE p_period
          WHEN 'weekly'  THEN lp.last_active_at > now() - interval '7 days'
          WHEN 'monthly' THEN lp.last_active_at > now() - interval '30 days'
          ELSE true
        END
  ORDER BY COALESCE(xt.total_xp, 0) DESC
  -- clamp: at least 1, at most 100; negative or zero inputs become 1
  LIMIT GREATEST(1, LEAST(COALESCE(p_limit, 20), 100))
)
SELECT
  r.lenser_id,
  r.handle,
  r.display_name,
  r.avatar_url,
  r.total_xp,
  r.current_level,
  (
    0.7 * log(GREATEST(1.0, r.total_xp::double precision))
    + 0.3 * log(GREATEST(1.0, COALESCE(re7.cnt, 0)::double precision))
  ) AS lenser_score,
  r.rank
FROM ranked r
LEFT JOIN LATERAL (
  SELECT count(*) AS cnt
  FROM (
    SELECT 1
    FROM content.reactions tr
    JOIN content.threads t ON t.id = tr.entity_id AND t.lenser_id = r.lenser_id
    WHERE tr.entity_type = 'thread'
      AND tr.created_at > now() - interval '7 days'
    UNION ALL
    SELECT 1
    FROM content.reactions pr
    JOIN lenses.lenses pt ON pt.id = pr.entity_id AND pt.lenser_id = r.lenser_id
    WHERE pr.entity_type = 'lens'
      AND pr.created_at > now() - interval '7 days'
  ) x
) re7 ON true
ORDER BY r.rank;
$$;


ALTER FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_preferences"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
    v_uid  uuid;
    v_row  lensers.preferences;
BEGIN
    v_uid := auth.uid();
    IF v_uid IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT p.*
      INTO v_row
      FROM lensers.preferences p
      JOIN lensers.profiles    pr ON pr.id = p.lenser_id
     WHERE pr.user_id = v_uid;

    IF NOT FOUND THEN
        RETURN '{}'::jsonb;
    END IF;

    RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_get_preferences"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_get_preferences"() IS 'Returns the authenticated user''s full preferences row as jsonb. Reads from lensers.preferences table (previously read from lensers.profiles.preferences jsonb). Updated in migration 20260322000059.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth', 'analytics', 'xp'
    AS $$
DECLARE
  v_viewer_uid uuid;
  v_subject RECORD;
  v_prefs lensers.preferences;
  v_access lensers.profile_access_level;
  v_viewer_profile_id uuid;
  v_rel_state jsonb;
  v_result jsonb;
  v_xp RECORD;
  v_min_xp bigint;
  v_max_xp bigint;
  v_stats RECORD;
  v_join RECORD;
  v_is_owner boolean := false;
  v_completion_score integer;
  v_activity jsonb;
BEGIN
  v_viewer_uid := auth.uid();

  -- Fetch subject profile (without visibility filter)
  SELECT p.id, p.handle, p.display_name, p.avatar_url, p.banner_url,
         p.bio, p.headline, p.status, p.visibility, p.created_at,
         p.type,
         p.user_id, p.deletion_requested_at, p.deletion_deadline_at,
         p.location, p.website_url
  INTO v_subject
  FROM lensers.profiles p
  WHERE p.handle = p_handle;

  IF NOT FOUND THEN
    RETURN jsonb_build_object(
      'route_state', 'UNAVAILABLE_PROFILE',
      'access_reason', 'not_found',
      'relationship_state', null,
      'profile', null
    );
  END IF;

  -- Fetch preferences row (1:1 with profile)
  SELECT * INTO v_prefs
  FROM lensers.preferences
  WHERE lenser_id = v_subject.id;

  -- Resolve access level
  v_access := lensers.fn_can_view_profile(v_viewer_uid, v_subject.id);

  -- Get viewer profile id for relationship state
  IF v_viewer_uid IS NOT NULL THEN
    SELECT p.id INTO v_viewer_profile_id
    FROM lensers.profiles p
    WHERE p.user_id = v_viewer_uid;
  END IF;

  -- Determine ownership
  v_is_owner := (v_viewer_uid IS NOT NULL AND v_viewer_uid = v_subject.user_id);

  -- Build relationship state
  IF v_viewer_profile_id IS NOT NULL AND v_viewer_profile_id <> v_subject.id THEN
    v_rel_state := lensers.fn_relationship_state(v_viewer_profile_id, v_subject.id);
  ELSE
    v_rel_state := null;
  END IF;

  -- UNAVAILABLE → minimal response
  IF v_access = 'UNAVAILABLE_PROFILE' THEN
    RETURN jsonb_build_object(
      'route_state', 'UNAVAILABLE_PROFILE',
      'access_reason', 'unavailable',
      'relationship_state', v_rel_state,
      'profile', null
    );
  END IF;

  -- Fetch XP, stats, join info for full/restricted/recovery
  SELECT t.total_xp, t.current_level, t.app_id INTO v_xp
  FROM xp.totals t WHERE t.lenser_id = v_subject.id;

  IF v_xp.app_id IS NOT NULL AND v_xp.current_level IS NOT NULL THEN
    SELECT lv.min_total_xp, lv.max_total_xp INTO v_min_xp, v_max_xp
    FROM xp.levels lv
    WHERE lv.app_id = v_xp.app_id AND lv.level = v_xp.current_level;
  END IF;

  SELECT s.thread_count, s.lens_count, s.follower_count, s.following_count, s.mutuals_count
  INTO v_stats
  FROM analytics.lenser_stats s WHERE s.lenser_id = v_subject.id;

  SELECT jl.join_order, jl.joined_at INTO v_join
  FROM analytics.lenser_join_log jl WHERE jl.lenser_id = v_subject.id;

  -- RESTRICTED → limited fields (no activity/completion)
  IF v_access = 'RESTRICTED_PROFILE' THEN
    RETURN jsonb_build_object(
      'route_state', 'RESTRICTED_PROFILE',
      'access_reason', 'private_not_following',
      'relationship_state', v_rel_state,
      'profile', jsonb_build_object(
        'id', v_subject.id,
        'handle', v_subject.handle,
        'display_name', v_subject.display_name,
        'avatar_url', v_subject.avatar_url,
        'type', v_subject.type,
        'visibility', v_subject.visibility,
        'total_xp', v_xp.total_xp,
        'current_level', v_xp.current_level,
        'follower_count', COALESCE(v_stats.follower_count, 0),
        'following_count', COALESCE(v_stats.following_count, 0),
        'join_order', v_join.join_order,
        'created_at', v_subject.created_at
      )
    );
  END IF;

  -- ── Completion score (owner only) ──────────────────────────────────────────
  IF v_is_owner THEN
    v_completion_score := 0;

    IF v_subject.bio IS NOT NULL AND length(trim(v_subject.bio)) > 0 THEN
      v_completion_score := v_completion_score + 25;
    END IF;
    IF v_subject.avatar_url IS NOT NULL AND length(trim(v_subject.avatar_url)) > 0 THEN
      v_completion_score := v_completion_score + 25;
    END IF;
    IF v_subject.location IS NOT NULL AND length(trim(v_subject.location)) > 0 THEN
      v_completion_score := v_completion_score + 15;
    END IF;
    IF v_subject.website_url IS NOT NULL AND length(trim(v_subject.website_url)) > 0 THEN
      v_completion_score := v_completion_score + 15;
    END IF;

    -- Battle participation
    IF EXISTS (
      SELECT 1 FROM battles.contenders bc
      JOIN battles.battles b ON b.id = bc.battle_id
      WHERE bc.contender_ref_id = v_subject.id
        AND b.status <> 'draft'::battles.battle_status_enum
    ) THEN
      v_completion_score := v_completion_score + 10;
    END IF;

    -- XP earned
    IF COALESCE((SELECT SUM(xp) FROM xp.events WHERE lenser_id = v_subject.id), 0) > 0 THEN
      v_completion_score := v_completion_score + 10;
    END IF;

    v_completion_score := LEAST(v_completion_score, 100);
  END IF;

  -- ── Activity timeline (365 days) ──────────────────────────────────────────
  SELECT COALESCE(jsonb_agg(
    jsonb_build_object('date', sub.date, 'count', sub.count)
    ORDER BY sub.date
  ), '[]'::jsonb)
  INTO v_activity
  FROM (
    WITH calendar AS (
      SELECT (current_date - s.i)::date AS day
      FROM generate_series(0, 364) AS s(i)
    ),
    events AS (
      -- Lens creations
      SELECT date_trunc('day', l.created_at)::date AS day
      FROM   lenses.lenses l
      WHERE  l.lenser_id = v_subject.id
        AND  l.visibility IN ('public', 'community')
        AND  l.status = 'published'
        AND  l.created_at >= (current_date - interval '364 days')

      UNION ALL

      -- Thread creations
      SELECT date_trunc('day', t.created_at)::date AS day
      FROM   content.threads t
      WHERE  t.lenser_id = v_subject.id
        AND  t.visibility IN ('public', 'community')
        AND  t.status = 'published'
        AND  t.created_at >= (current_date - interval '364 days')

      UNION ALL

      -- Reactions (skipped when hide_actions = true)
      SELECT date_trunc('day', rx.created_at)::date AS day
      FROM   content.reactions rx
      WHERE  rx.lenser_id = v_subject.id
        AND  COALESCE(v_prefs.hide_actions, false) = false
        AND  rx.created_at >= (current_date - interval '364 days')
    ),
    daily_counts AS (
      SELECT day, count(*) AS cnt
      FROM   events
      GROUP BY day
    )
    SELECT to_char(c.day, 'YYYY-MM-DD') AS date,
           COALESCE(dc.cnt, 0)          AS count
    FROM   calendar c
    LEFT JOIN daily_counts dc ON dc.day = c.day
  ) sub;

  -- Determine access reason
  DECLARE
    v_reason text;
  BEGIN
    IF v_access = 'OWNER_RECOVERY_PROFILE' THEN
      v_reason := 'owner_recovery';
    ELSIF v_subject.visibility = 'private' THEN
      v_reason := 'private_following';
    ELSE
      v_reason := 'public';
    END IF;

    -- FULL / OWNER_RECOVERY → all fields
    v_result := jsonb_build_object(
      'route_state', v_access::text,
      'access_reason', v_reason,
      'relationship_state', v_rel_state,
      'completion_score', v_completion_score,
      'activity_timeline', v_activity,
      'profile', jsonb_build_object(
        'id', v_subject.id,
        'handle', v_subject.handle,
        'display_name', v_subject.display_name,
        'avatar_url', v_subject.avatar_url,
        'banner_url', v_subject.banner_url,
        'bio', v_subject.bio,
        'headline', v_subject.headline,
        'status', v_subject.status,
        'type', v_subject.type,
        'visibility', v_subject.visibility,
        'created_at', v_subject.created_at,
        'total_xp', v_xp.total_xp,
        'current_level', v_xp.current_level,
        'min_xp', v_min_xp,
        'max_xp', v_max_xp,
        'app_id', v_xp.app_id,
        'thread_count', COALESCE(v_stats.thread_count, 0),
        'lens_count', COALESCE(v_stats.lens_count, 0),
        'follower_count', COALESCE(v_stats.follower_count, 0),
        'following_count', COALESCE(v_stats.following_count, 0),
        'join_order', v_join.join_order,
        'joined_at', v_join.joined_at,
        'hide_actions', COALESCE(v_prefs.hide_actions, false),
        'content_visibility', COALESCE(v_prefs.content_visibility, 'public'),
        'deletion_deadline_at', v_subject.deletion_deadline_at
      )
    );

    RETURN v_result;
  END;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_verdict     text;
  v_result      jsonb;
  v_route_state text;
BEGIN
  -- ── Governance gate ────────────────────────────────────────────────────────
  -- Validate the incoming handle against the identity_gov registry before
  -- touching the profile table. A 'deny' verdict returns null — same as the
  -- not-found or private-profile path — preventing namespace enumeration.
  SELECT verdict
  INTO   v_verdict
  FROM   identity_gov.fn_validate_handle(p_handle)
  LIMIT  1;

  IF v_verdict = 'deny' THEN
    RETURN null;
  END IF;

  -- ── Normal profile lookup ─────────────────────────────────────────────────
  v_result      := public.fn_lensers_get_profile(p_handle);
  v_route_state := v_result->>'route_state';

  IF v_route_state = 'FULL_PROFILE' THEN
    RETURN v_result->'profile';
  ELSE
    RETURN null;
  END IF;

EXCEPTION WHEN OTHERS THEN
  -- Fail safe: governance error or profile error returns null, not a 500.
  RETURN null;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") IS 'Public profile lookup with identity_gov namespace guard. Returns null for denied handles (reserved, impersonation) and for non-FULL_PROFILE route states (private, deactivated, deleted). Callers cannot distinguish denied-by-governance from not-found.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_xp" bigint, "current_level" integer, "lenser_score" double precision, "tag_overlap_score" double precision)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'lenses', 'content', 'xp'
    AS $$
WITH
  interest_tags AS (
    SELECT tag_id FROM lensers.tag_follows WHERE lenser_id = p_lenser_id
  ),
  already_following AS (
    SELECT target_profile_id AS following_id
    FROM lensers.relationships
    WHERE source_profile_id = p_lenser_id
      AND status = 'accepted'
  ),
  thread_author_tags AS (
    SELECT
      t.lenser_id,
      COUNT(DISTINCT it.tag_id)::float
        / GREATEST((SELECT COUNT(*) FROM interest_tags), 1) AS tag_score
    FROM content.threads    t
    JOIN content.tag_map    tm ON tm.entity_id = t.id AND tm.entity_type = 'thread'
    JOIN interest_tags      it ON it.tag_id = tm.tag_id
    WHERE t.visibility = 'public'::content.visibility_enum
    GROUP BY t.lenser_id
  ),
  prompt_author_tags AS (
    SELECT
      pt.lenser_id,
      COUNT(DISTINCT it.tag_id)::float
        / GREATEST((SELECT COUNT(*) FROM interest_tags), 1) AS tag_score
    FROM lenses.lenses pt
    JOIN content.tag_map  tm ON tm.entity_id = pt.id AND tm.entity_type = 'lens'
    JOIN interest_tags    it ON it.tag_id = tm.tag_id
    WHERE pt.visibility = 'public'::content.visibility_enum
    GROUP BY pt.lenser_id
  ),
  author_tag_agg AS (
    SELECT lenser_id, AVG(tag_score) AS tag_overlap_score
    FROM (
      SELECT lenser_id, tag_score FROM thread_author_tags
      UNION ALL
      SELECT lenser_id, tag_score FROM prompt_author_tags
    ) combined
    GROUP BY lenser_id
  ),
  tag_pool AS (
    SELECT
      lp.id          AS lenser_id,
      lp.handle,
      lp.display_name,
      lp.avatar_url,
      COALESCE(xt.total_xp, 0)::bigint    AS total_xp,
      COALESCE(xt.current_level, 1)        AS current_level,
      COALESCE(agg.tag_overlap_score, 0.0) AS tag_overlap_score
    FROM author_tag_agg agg
    JOIN lensers.profiles lp ON lp.id = agg.lenser_id
    LEFT JOIN (
      SELECT t.lenser_id, SUM(t.total_xp) AS total_xp, MAX(t.current_level) AS current_level
      FROM xp.totals t GROUP BY t.lenser_id
    ) xt ON xt.lenser_id = lp.id
    WHERE lp.status                = 'active'
      AND lp.visibility            = 'public'
      AND lp.onboarding_step       = 2
      AND lp.deletion_requested_at IS NULL
      AND lp.id <> p_lenser_id
      AND lp.id NOT IN (SELECT following_id FROM already_following)
  ),
  xp_pool AS (
    SELECT
      lp.id          AS lenser_id,
      lp.handle,
      lp.display_name,
      lp.avatar_url,
      COALESCE(xt.total_xp, 0)::bigint AS total_xp,
      COALESCE(xt.current_level, 1)     AS current_level,
      0.0::double precision             AS tag_overlap_score
    FROM lensers.profiles lp
    LEFT JOIN (
      SELECT t.lenser_id, SUM(t.total_xp) AS total_xp, MAX(t.current_level) AS current_level
      FROM xp.totals t GROUP BY t.lenser_id
    ) xt ON xt.lenser_id = lp.id
    WHERE lp.status                = 'active'
      AND lp.visibility            = 'public'
      AND lp.onboarding_step       = 2
      AND lp.deletion_requested_at IS NULL
      AND lp.id <> p_lenser_id
      AND lp.id NOT IN (SELECT following_id FROM already_following)
  ),
  chosen AS (
    SELECT * FROM tag_pool
    WHERE (SELECT COUNT(*) FROM tag_pool) > 0
    UNION ALL
    SELECT * FROM xp_pool
    WHERE (SELECT COUNT(*) FROM tag_pool) = 0
  ),
  top_candidates AS (
    SELECT * FROM chosen
    ORDER BY tag_overlap_score DESC, total_xp DESC
    LIMIT LEAST(p_limit, 50) * 2
  )
SELECT
  tc.lenser_id,
  tc.handle,
  tc.display_name,
  tc.avatar_url,
  tc.total_xp,
  tc.current_level,
  (
    0.7 * log(GREATEST(1.0, tc.total_xp::double precision))
    + 0.3 * log(GREATEST(1.0, COALESCE(re7.cnt, 0)::double precision))
  ) AS lenser_score,
  tc.tag_overlap_score
FROM top_candidates tc
LEFT JOIN LATERAL (
  SELECT count(*) AS cnt FROM (
    SELECT 1
    FROM content.reactions tr
    JOIN content.threads t ON t.id = tr.entity_id AND t.lenser_id = tc.lenser_id
    WHERE tr.entity_type = 'thread'
      AND tr.created_at > now() - interval '7 days'
    UNION ALL
    SELECT 1
    FROM content.reactions pr
    JOIN lenses.lenses pt ON pt.id = pr.entity_id AND pt.lenser_id = tc.lenser_id
    WHERE pr.entity_type = 'lens'
      AND pr.created_at > now() - interval '7 days'
  ) x
) re7 ON true
ORDER BY (
  0.60 * tc.tag_overlap_score
  + 0.40 * LEAST(
    (0.7 * log(GREATEST(1.0, tc.total_xp::double precision))
     + 0.3 * log(GREATEST(1.0, COALESCE(re7.cnt, 0)::double precision)))
    / 5.0, 1.0)
) DESC
LIMIT LEAST(p_limit, 50);
$$;


ALTER FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer DEFAULT 10) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_xp" bigint, "current_level" integer, "lenser_score" double precision)
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'lensers', 'content', 'xp'
    AS $$
  SELECT
    lenser_id,
    handle,
    display_name,
    avatar_url,
    total_xp,
    current_level,
    lenser_score
  FROM lensers.vw_lensers_score
  ORDER BY lenser_score DESC
  LIMIT LEAST(p_limit, 50);
$$;


ALTER FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM lensers.relationships r
    JOIN lensers.profiles p ON p.id = r.source_profile_id AND p.user_id = auth.uid()
    WHERE r.target_profile_id = p_target_id
      AND r.status = 'accepted'
  );
$$;


ALTER FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_list"("p_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "bio" "text", "type" "lensers"."lenser_type", "ai_model_id" "uuid", "created_at" timestamp with time zone, "engagement" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'analytics', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    p.id,
    p.handle,
    p.display_name,
    p.avatar_url,
    p.bio,
    p.type,
    p.ai_model_id,
    p.created_at,
    CASE
      WHEN s.lenser_id IS NOT NULL THEN to_jsonb(s)
      ELSE NULL::jsonb
    END AS engagement
  FROM lensers.profiles p
  LEFT JOIN analytics.lenser_stats s ON s.lenser_id = p.id
  WHERE
    p.status              = 'active'
    AND p.deletion_requested_at IS NULL
    AND p.onboarding_step >= 1
    AND p.visibility      = 'public'
    AND (p_type IS NULL OR p.type = p_type::lensers.lenser_type)
  ORDER BY p.created_at DESC
  LIMIT  LEAST(p_limit, 100)
  OFFSET GREATEST(p_offset, 0);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_request_deletion"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.profiles
  SET
    deletion_requested_at = NOW(),
    updated_at = NOW()
  WHERE user_id = v_uid
    AND status = 'active'
    AND deletion_requested_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Active Lenser profile not found or deletion already requested';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_request_deletion"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
  rec jsonb;
  v_id uuid;
  v_platform lensers.lenser_social_platform;
  v_url text;
  v_label text;
  v_incoming_ids uuid[];
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
    AND status = 'active'
    AND deletion_requested_at IS NULL;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Lenser profile not found';
  END IF;

  -- Collect incoming IDs
  SELECT array_agg((elem->>'id')::uuid)
  INTO v_incoming_ids
  FROM jsonb_array_elements(p_links) elem
  WHERE elem ? 'id';

  -- Delete missing
  DELETE FROM lensers.social_links
  WHERE lenser_id = v_lenser_id
    AND (v_incoming_ids IS NULL OR NOT (id = ANY (v_incoming_ids)));

  -- Upsert loop
  FOR rec IN SELECT * FROM jsonb_array_elements(p_links)
  LOOP
    v_id := COALESCE((rec->>'id')::uuid, gen_random_uuid());
    v_platform := (rec->>'platform')::lensers.lenser_social_platform;
    v_url := rec->>'url';
    v_label := rec->>'label';

    INSERT INTO lensers.social_links (id, lenser_id, platform, url, label)
    VALUES (v_id, v_lenser_id, v_platform, v_url, v_label)
    ON CONFLICT (id) DO UPDATE
      SET platform = EXCLUDED.platform,
          url      = EXCLUDED.url,
          label    = EXCLUDED.label;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
BEGIN
  IF auth.uid() IS NULL THEN
    RAISE EXCEPTION 'not_authenticated';
  END IF;

  IF NOT p_kvkk_approved THEN
    RAISE EXCEPTION 'kvkk_not_approved';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM lensers.profiles WHERE user_id = auth.uid()
  ) THEN
    RAISE EXCEPTION 'not_a_lenser';
  END IF;

  UPDATE lensers.profiles
  SET is_in_waiting_list = NOT is_in_waiting_list
  WHERE user_id = auth.uid();
END;
$$;


ALTER FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  v_result := public.fn_remove_follow(p_following_id);
  RETURN jsonb_build_object('following', false);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'ai', 'auth'
    AS $$
DECLARE
    v_uid              uuid;
    v_lenser_id        uuid;
    v_active_lenser_id uuid;
    v_selected_key_id  uuid;
    v_key_owner        uuid;
    v_row              lensers.preferences;
BEGIN
    v_uid := auth.uid();
    IF v_uid IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT id INTO v_lenser_id
      FROM lensers.profiles
     WHERE user_id = v_uid
       AND type    = 'human'
     LIMIT 1;

    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Profile not found';
    END IF;

    -- Resolve the active workspace context; may differ from v_lenser_id when
    -- the user has switched into an AI agent workspace.
    v_active_lenser_id := lensers.get_auth_lenser_id();

    -- Validate selected_api_key_id ownership when provided
    IF p_data ? 'selected_api_key_id' AND p_data->>'selected_api_key_id' IS NOT NULL THEN
        v_selected_key_id := (p_data->>'selected_api_key_id')::uuid;

        SELECT lenser_id INTO v_key_owner
          FROM ai.keys
         WHERE id = v_selected_key_id
           AND is_active = true;

        IF NOT FOUND THEN
            RAISE EXCEPTION 'API key not found or is not active'
                USING ERRCODE = 'P0002';
        END IF;

        -- Allow keys belonging to the human profile OR the currently active
        -- workspace (e.g. an AI agent the user has switched into).
        IF v_key_owner <> v_lenser_id AND v_key_owner <> v_active_lenser_id THEN
            RAISE EXCEPTION 'API key does not belong to this lenser'
                USING ERRCODE = 'P0003';
        END IF;
    END IF;

    UPDATE lensers.preferences
    SET
        language              = CASE WHEN p_data ? 'language'              THEN p_data->>'language'                                              ELSE language              END,
        theme                 = CASE WHEN p_data ? 'theme'                 THEN (p_data->>'theme')::text                                         ELSE theme                 END,
        notifications         = CASE WHEN p_data ? 'notifications'         THEN p_data->'notifications'                                          ELSE notifications         END,
        sidebar               = CASE WHEN p_data ? 'sidebar'               THEN p_data->'sidebar'                                                ELSE sidebar               END,
        content_visibility    = CASE WHEN p_data ? 'content_visibility'    THEN p_data->>'content_visibility'                                     ELSE content_visibility    END,
        email_digest          = CASE WHEN p_data ? 'email_digest'          THEN (p_data->>'email_digest')::boolean                               ELSE email_digest          END,
        ai_provider_key       = CASE WHEN p_data ? 'ai_provider_key'       THEN p_data->>'ai_provider_key'                                       ELSE ai_provider_key       END,
        ai_model_key          = CASE WHEN p_data ? 'ai_model_key'          THEN p_data->>'ai_model_key'                                          ELSE ai_model_key          END,
        ai_persona            = CASE WHEN p_data ? 'ai_persona'            THEN p_data->>'ai_persona'                                            ELSE ai_persona            END,
        ai_ruleset            = CASE WHEN p_data ? 'ai_ruleset'            THEN p_data->'ai_ruleset'                                             ELSE ai_ruleset            END,
        wallet_mode           = CASE WHEN p_data ? 'wallet_mode'           THEN (p_data->>'wallet_mode')::lensers.wallet_mode_enum               ELSE wallet_mode           END,
        ai_data_usage         = CASE WHEN p_data ? 'ai_data_usage'         THEN (p_data->>'ai_data_usage')::boolean                              ELSE ai_data_usage         END,
        hide_actions          = CASE WHEN p_data ? 'hide_actions'          THEN (p_data->>'hide_actions')::boolean                               ELSE hide_actions          END,
        cron_config           = CASE WHEN p_data ? 'cron_config'           THEN p_data->'cron_config'                                            ELSE cron_config           END,
        selected_api_key_id   = CASE
                                    WHEN p_data ? 'selected_api_key_id' AND p_data->>'selected_api_key_id' IS NULL
                                        THEN NULL
                                    WHEN p_data ? 'selected_api_key_id'
                                        THEN (p_data->>'selected_api_key_id')::uuid
                                    ELSE selected_api_key_id
                                END,
        updated_at            = now()
    WHERE lenser_id = v_lenser_id
    RETURNING * INTO v_row;

    IF NOT FOUND THEN
        -- Auto-create missing row for legacy profiles, then retry
        INSERT INTO lensers.preferences (lenser_id)
        VALUES (v_lenser_id)
        ON CONFLICT (lenser_id) DO NOTHING;

        UPDATE lensers.preferences
        SET
            language              = CASE WHEN p_data ? 'language'              THEN p_data->>'language'           ELSE language              END,
            theme                 = CASE WHEN p_data ? 'theme'                 THEN p_data->>'theme'              ELSE theme                 END,
            notifications         = CASE WHEN p_data ? 'notifications'         THEN p_data->'notifications'       ELSE notifications         END,
            sidebar               = CASE WHEN p_data ? 'sidebar'               THEN p_data->'sidebar'             ELSE sidebar               END,
            content_visibility    = CASE WHEN p_data ? 'content_visibility'    THEN p_data->>'content_visibility' ELSE content_visibility    END,
            email_digest          = CASE WHEN p_data ? 'email_digest'          THEN (p_data->>'email_digest')::boolean ELSE email_digest     END,
            ai_provider_key       = CASE WHEN p_data ? 'ai_provider_key'       THEN p_data->>'ai_provider_key'    ELSE ai_provider_key       END,
            ai_model_key          = CASE WHEN p_data ? 'ai_model_key'          THEN p_data->>'ai_model_key'       ELSE ai_model_key          END,
            ai_persona            = CASE WHEN p_data ? 'ai_persona'            THEN p_data->>'ai_persona'         ELSE ai_persona            END,
            ai_ruleset            = CASE WHEN p_data ? 'ai_ruleset'            THEN p_data->'ai_ruleset'          ELSE ai_ruleset            END,
            wallet_mode           = CASE WHEN p_data ? 'wallet_mode'           THEN (p_data->>'wallet_mode')::lensers.wallet_mode_enum ELSE wallet_mode END,
            ai_data_usage         = CASE WHEN p_data ? 'ai_data_usage'         THEN (p_data->>'ai_data_usage')::boolean ELSE ai_data_usage  END,
            hide_actions          = CASE WHEN p_data ? 'hide_actions'          THEN (p_data->>'hide_actions')::boolean ELSE hide_actions     END,
            cron_config           = CASE WHEN p_data ? 'cron_config'           THEN p_data->'cron_config'         ELSE cron_config           END,
            selected_api_key_id   = CASE
                                        WHEN p_data ? 'selected_api_key_id' AND p_data->>'selected_api_key_id' IS NULL
                                            THEN NULL
                                        WHEN p_data ? 'selected_api_key_id'
                                            THEN (p_data->>'selected_api_key_id')::uuid
                                        ELSE selected_api_key_id
                                    END,
            updated_at            = now()
        WHERE lenser_id = v_lenser_id
        RETURNING * INTO v_row;
    END IF;

    RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") IS 'Partial-patch update of the authenticated user''s lensers.preferences row. Accepts any subset of preference fields as jsonb — only supplied keys are written. When selected_api_key_id is provided, validates the key is active and owned by this lenser''s human profile OR by the currently active workspace context (e.g. an AI agent the user has switched into). SECURITY DEFINER — runs as postgres, bypasses table-level permission check. Auto-creates the preferences row if missing (legacy profile guard). Added in migration 000061. Updated in 000063 to support selected_api_key_id.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_row lensers.profiles;
  v_language text;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.profiles
  SET
    display_name            = CASE WHEN p_data ? 'display_name'            THEN (p_data->>'display_name')                         ELSE display_name            END,
    avatar_url              = CASE WHEN p_data ? 'avatar_url'              THEN (p_data->>'avatar_url')                           ELSE avatar_url              END,
    banner_url              = CASE WHEN p_data ? 'banner_url'              THEN (p_data->>'banner_url')                           ELSE banner_url              END,
    bio                     = CASE WHEN p_data ? 'bio'                     THEN (p_data->>'bio')                                  ELSE bio                     END,
    headline                = CASE WHEN p_data ? 'headline'                THEN (p_data->>'headline')                             ELSE headline                END,
    onboarding_step         = CASE WHEN p_data ? 'onboarding_step'         THEN (p_data->>'onboarding_step')::smallint            ELSE onboarding_step         END,
    onboarding_completed_at = CASE WHEN p_data ? 'onboarding_completed_at' THEN (p_data->>'onboarding_completed_at')::timestamptz ELSE onboarding_completed_at END,
    visibility              = CASE WHEN p_data ? 'visibility'              THEN (p_data->>'visibility')::lensers.lenser_visibility ELSE visibility              END,
    updated_at              = now()
  WHERE id = (
    SELECT id FROM lensers.profiles
    WHERE user_id = v_uid
    ORDER BY created_at ASC
    LIMIT 1
  )
  RETURNING * INTO v_row;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Profile not found';
  END IF;

  IF p_data ? 'preferred_language' THEN
    v_language := COALESCE(NULLIF(btrim(p_data->>'preferred_language'), ''), 'en');
    INSERT INTO lensers.preferences (lenser_id, language)
    VALUES (v_row.id, v_language)
    ON CONFLICT (lenser_id) DO UPDATE
    SET language = EXCLUDED.language,
        updated_at = now();
  END IF;

  RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text" DEFAULT NULL::"text", "p_parent_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "lenses"."versions"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'public'
    AS $$
  SELECT * FROM lenses.fn_upsert_draft_version(
    p_lens_id, p_template_body, p_changelog, p_parent_version_id
  );
$$;


ALTER FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") RETURNS SETOF "lenses"."versions"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT * FROM lenses.fn_list_versions(p_lens_id);
$$;


ALTER FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
  SELECT lenses.fn_publish_version(p_version_id);
$$;


ALTER FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_starred"() RETURNS SETOF "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lenses', 'lensers'
    AS $$
  SELECT jsonb_build_object(
    'id', l.id,
    'slug', lower(replace(COALESCE(et.title, l.id::text), ' ', '-')),
    'title', COALESCE(et.title, 'Untitled'),
    'author_username', p.handle,
    'starred_at', r.created_at,
    'lifecycle', public.fn_artifact_lifecycle_status('lens', l.id)
  )
  FROM content.reactions r
  JOIN lenses.lenses l ON l.id = r.entity_id
  LEFT JOIN content.entity_translations et
    ON et.entity_id = l.id
   AND et.entity_type = 'lens'::content.entity_type_enum
   AND et.is_original = TRUE
  LEFT JOIN lensers.profiles p ON p.id = l.lenser_id
  WHERE r.entity_type = 'lens'::content.entity_type_enum
    AND r.reaction = 'saved'::content.reaction_enum
    AND r.lenser_id = lensers.get_auth_lenser_id()
    AND public.fn_artifact_can_view('lens', l.id)
  ORDER BY r.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_lenses_starred"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lifecycle_is_service_role"() RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
  SELECT COALESCE(current_setting('request.jwt.claim.role', true), auth.role(), '') = 'service_role';
$$;


ALTER FUNCTION "public"."fn_lifecycle_is_service_role"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "action_type" "text", "context_ref_type" "text", "context_ref_id" "uuid", "result" "text", "metadata" "jsonb", "occurred_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT al.id, al.ai_lenser_id, al.action_type, al.context_ref_type,
         al.context_ref_id, al.result, al.metadata, al.occurred_at
  FROM agents.action_logs al
  WHERE al.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR al.occurred_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = al.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY al.occurred_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ri.*)
  FROM   agents.run_incidents ri
  WHERE  ri.ai_lenser_id = p_ai_lenser_id
    AND  (p_cursor IS NULL OR ri.created_at < p_cursor)
    AND  (
           agents.can_manage_ai_lenser(p_ai_lenser_id)
           OR public.fn_is_super_admin()
         )
  ORDER BY ri.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 20), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Keyset-paginated list of run_incidents for an AI lenser. Returns up to p_limit rows (max 200) ordered by created_at DESC. Caller must own the agent or be a super admin.';



CREATE OR REPLACE FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "is_default" boolean, "category_tags" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT lb.id, lb.ai_lenser_id, lb.lens_id, lb.version_id,
         lb.is_default, lb.category_tags, lb.created_at
  FROM agents.lens_bindings lb
  WHERE lb.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY lb.is_default DESC, lb.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50, "p_include_redacted" boolean DEFAULT false) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mv.*)
  FROM agents.memories_v mv
  WHERE mv.profile_id = p_profile_id
    AND (p_scope IS NULL OR mv.scope = p_scope)
    AND (p_include_redacted OR mv.is_redacted = false)
    AND (
      EXISTS (
        SELECT 1 FROM agents.memory_profiles mp
        JOIN agents.ownerships o ON o.ai_lenser_id = mp.ai_lenser_id
        WHERE mp.id = p_profile_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY mv.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "model_id" "uuid", "is_default" boolean, "category_tags" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT mb.id, mb.ai_lenser_id, mb.model_id, mb.is_default,
         mb.category_tags, mb.created_at
  FROM agents.model_bindings mb
  WHERE mb.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY mb.is_default DESC, mb.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "owner_lenser_id" "uuid", "role" "text", "permission_scope" "text"[], "granted_at" timestamp with time zone, "revoked_at" timestamp with time zone, "owner_handle" "text", "owner_display_name" "text", "owner_avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT
    o.id,
    o.ai_lenser_id,
    o.owner_lenser_id,
    o.role,
    o.permission_scope,
    o.granted_at,
    o.revoked_at,
    p.handle,
    p.display_name,
    p.avatar_url
  FROM agents.ownerships o
  JOIN lensers.profiles p
    ON p.id = o.owner_lenser_id
  WHERE o.ai_lenser_id = p_ai_lenser_id
    AND o.revoked_at IS NULL
    AND agents.can_manage_ai_lenser(p_ai_lenser_id)
  ORDER BY CASE o.role
    WHEN 'owner' THEN 1
    WHEN 'co_owner' THEN 2
    ELSE 3
  END,
  o.granted_at ASC;
$$;


ALTER FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") IS 'Lists active human delegates for an AI Lenser, including the primary owner, co-owners, and scoped operators.';



CREATE OR REPLACE FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ars.*)
  FROM agents.agent_run_steps ars
  JOIN agents.team_runs tr ON tr.id = ars.team_run_id
  WHERE ars.team_run_id = p_team_run_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = tr.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY ars.started_at ASC;
$$;


ALTER FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "name" "text", "description" "text", "status" "text", "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT t.id, t.ai_lenser_id, t.name, t.description, t.status,
         t.is_active, t.created_at, t.updated_at
  FROM agents.teams t
  WHERE t.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR t.created_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY t.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" "uuid" DEFAULT NULL::"uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ti.*)
  FROM   agents.tool_invocations ti
  WHERE  ti.ai_lenser_id = p_ai_lenser_id
    AND  (p_cursor IS NULL OR ti.id < p_cursor)
    AND  (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE  o.ai_lenser_id = p_ai_lenser_id
          AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND  o.revoked_at IS NULL
      )
      OR public.is_admin()
    )
  ORDER  BY ti.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(v.*)
  FROM agents.v_agent_profile v
  WHERE v.owner_lenser_id = p_owner_lenser_id
    AND (p_owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY v.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ar.*)
  FROM agents.approval_requests_v ar
  WHERE ar.ai_lenser_id = p_ai_lenser_id
    AND (p_approval_status IS NULL OR ar.approval_status = p_approval_status)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY ar.requested_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_automation_rules"("p_limit" integer DEFAULT 100, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "name" "text", "match_event_type" "text", "match_filter" "jsonb", "action_kind" "text", "action_config" "jsonb", "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  SELECT
    tr.id,
    tr.lenser_id,
    tr.name,
    tr.match_event_type,
    tr.match_filter,
    tr.action_kind,
    tr.action_config,
    tr.is_active,
    tr.created_at,
    tr.updated_at
  FROM automation.trigger_rules tr
  WHERE tr.lenser_id = auth.uid()
    AND (p_cursor IS NULL OR tr.created_at < p_cursor)
  ORDER BY tr.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: list automation trigger rules for the current user. Keyset-paginated by created_at DESC. Max 200 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_list_battle_templates"("p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "title" "text", "description" "text", "task_prompt" "text", "is_public" boolean, "max_contenders" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT t.id, t.title, t.description, t.task_prompt,
         t.is_public, t.max_contenders, t.created_at, t.updated_at
  FROM battles.templates t
  WHERE t.deleted_at IS NULL
    AND (p_cursor IS NULL OR t.created_at < p_cursor)
  ORDER BY t.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ec.*)
  FROM agents.evaluation_cases ec
  JOIN agents.evaluations ev ON ev.id = ec.evaluation_id
  WHERE ec.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY ec.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(erub.*)
  FROM agents.evaluation_rubrics erub
  JOIN agents.evaluations ev ON ev.id = erub.evaluation_id
  WHERE erub.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY erub.version DESC;
$$;


ALTER FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(er.*)
  FROM agents.evaluation_runs er
  JOIN agents.evaluations ev ON ev.id = er.evaluation_id
  WHERE er.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY er.started_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ev.*)
  FROM agents.evaluations ev
  WHERE ev.owner_lenser_id = p_owner_lenser_id
    AND (p_cursor IS NULL OR ev.created_at < p_cursor)
    AND (p_owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY ev.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer DEFAULT 50) RETURNS TABLE("device_id" "uuid", "hostname" "text", "daemon_version" "text", "last_seen_at" timestamp with time zone, "approved_at" timestamp with time zone, "revoked_at" timestamp with time zone, "kill_switch" boolean, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT * FROM agents.fn_list_gateway_devices(p_limit);
$$;


ALTER FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean DEFAULT false) RETURNS TABLE("id" "uuid", "lens_id" "uuid", "version_number" integer, "status" "text", "changelog" "text", "parameter_count" integer, "published_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    v.id, v.lens_id, v.version_number, v.status::text,
    v.changelog, v.parameter_count, v.published_at, v.created_at
  FROM lenses.vw_lens_version_history v
  JOIN lenses.lenses l ON l.id = v.lens_id
  WHERE v.lens_id = p_lens_id
    AND (
      l.lenser_id = lensers.get_auth_lenser_id()
      OR (l.visibility = 'public' AND v.status = 'published')
    )
    AND (p_include_archived OR v.status <> 'archived')
  ORDER BY v.version_number DESC;
$$;


ALTER FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) IS 'Security wrapper: list versions for a lens. Owner sees all (optionally archived); others see published versions of public lenses only.';



CREATE OR REPLACE FUNCTION "public"."fn_list_media_objects"("p_limit" integer DEFAULT 200, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "owner_lenser_id" "uuid", "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "content_text" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'media', 'lensers'
    AS $$
  SELECT
    o.id, o.workspace_id, o.owner_lenser_id, o.bucket, o.object_key,
    o.external_url, o.mime_type, o.media_type, o.name, o.content_text,
    o.byte_size, o.visibility, o.lifecycle_state, o.metadata,
    o.created_at, o.updated_at
  FROM media.objects o
  WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
    AND o.lifecycle_state <> 'deleted'
    AND (p_cursor IS NULL OR o.created_at < p_cursor)
  ORDER BY o.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 200), 1), 500);
$$;


ALTER FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: list non-deleted media objects owned by the current user. Keyset-paginated by created_at DESC. Max 500 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer DEFAULT 50) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mal.*)
  FROM agents.memory_access_logs mal
  WHERE mal.memory_id = p_memory_id
  ORDER BY mal.accessed_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mp.*)
  FROM agents.memory_profiles mp
  WHERE mp.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY mp.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mop.*)
  FROM agents.model_profiles mop
  WHERE mop.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY mop.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "text", "parent_lens_id" "uuid", "forked_from_execution_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    l.id, l.lenser_id, l.visibility, l.parent_lens_id,
    l.forked_from_execution_id, l.created_at, l.updated_at
  FROM lenses.lenses l
  WHERE l.lenser_id = lensers.get_auth_lenser_id()
    AND l.visibility <> 'public'
    AND (p_cursor IS NULL OR l.created_at < p_cursor)
  ORDER BY l.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: list the current user''s non-public lenses (private/draft). Keyset-paginated by created_at DESC. Max 200 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(pp.*)
  FROM agents.personality_profiles pp
  WHERE pp.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY pp.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 100, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(pe.*)
  FROM   agents.policy_evaluations pe
  WHERE  pe.ai_lenser_id = p_ai_lenser_id
    AND  (p_cursor IS NULL OR pe.evaluated_at < p_cursor)
    AND  (
           agents.can_manage_ai_lenser(p_ai_lenser_id)
           OR public.fn_is_super_admin()
         )
  ORDER BY pe.evaluated_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
$$;


ALTER FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Keyset-paginated list of policy_evaluations for an AI lenser ordered by evaluated_at DESC. Returns up to p_limit rows (max 500). Caller must own the agent or be a super admin.';



CREATE OR REPLACE FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "title" "text", "description" "text", "task_prompt" "text", "category" "text", "is_public" boolean, "max_contenders" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT t.id, t.title, t.description,
         -- task_prompt only returned to authenticated callers; anon gets NULL to
         -- prevent accidental full-prompt exposure of mistakenly-public templates.
         CASE WHEN auth.uid() IS NOT NULL THEN t.task_prompt ELSE NULL END AS task_prompt,
         t.category, t.is_public, t.max_contenders, t.created_at, t.updated_at
    FROM battles.templates t
   WHERE t.is_public = true
     AND t.deleted_at IS NULL
     AND (p_category IS NULL OR t.category = p_category)
   ORDER BY t.created_at DESC
   LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(al.*)
  FROM agents.action_logs al
  WHERE al.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR al.occurred_at < p_cursor)
    AND al.result = 'error'
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY al.occurred_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text" DEFAULT NULL::"text", "p_resolved" boolean DEFAULT NULL::boolean, "p_limit" integer DEFAULT 100) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ri.*)
  FROM agents.run_incidents ri
  JOIN agents.run_reports rr ON rr.id = ri.run_report_id
  WHERE ri.run_report_id = p_run_report_id
    AND (p_severity IS NULL OR ri.severity::text = p_severity)
    AND (p_resolved IS NULL
         OR (p_resolved = true AND ri.resolved_at IS NOT NULL)
         OR (p_resolved = false AND ri.resolved_at IS NULL))
    AND (
      rr.ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
          AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY ri.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
$$;


ALTER FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT to_jsonb(rr.*)
  FROM agents.run_reports rr
  WHERE rr.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR rr.created_at < p_cursor)
  ORDER BY rr.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(sr.*)
  FROM agents.scratchpad_runs sr
  WHERE sr.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR sr.started_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY sr.started_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(te.*)
  FROM agents.team_edges te
  JOIN agents.teams t ON t.id = te.team_id
  WHERE te.team_id = p_team_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = t.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY te.created_at;
$$;


ALTER FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_template_workflows"("p_limit" integer DEFAULT 12, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "node_count" bigint, "reaction_totals" "jsonb", "fork_count" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "author_handle" "text", "author_display_name" "text", "kinds" "text"[])
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers'
    AS $$
  WITH tmpl AS (
    SELECT tm.entity_id AS workflow_id, tm.created_at AS marked_at
    FROM content.tag_map tm
    JOIN content.tags t ON t.id = tm.tag_id
    WHERE tm.entity_type = 'workflow'::content.entity_type_enum
      AND t.slug = 'template'
  )
  SELECT
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility::text,
    (
      SELECT count(*) FROM lenses.workflow_nodes wn WHERE wn.workflow_id = w.id
    ) AS node_count,
    w.reaction_totals,
    w.fork_count,
    w.created_at,
    w.updated_at,
    p.handle             AS author_handle,
    p.display_name       AS author_display_name,
    COALESCE(
      (
        SELECT array_agg(DISTINCT t.slug ORDER BY t.slug)
        FROM   lenses.workflow_nodes wn
        JOIN   lenses.lenses   ll  ON ll.id  = wn.lens_id
        JOIN   content.tag_map tm2 ON tm2.entity_id   = ll.id
                                   AND tm2.entity_type = 'lens'::content.entity_type_enum
        JOIN   content.tags    t   ON t.id = tm2.tag_id
                                   AND t.slug IN (
                                     'text','image','video','audio','music',
                                     'research','pdf','transform','orchestration',
                                     'validation','routing',
                                     'code','data','planning','community','documentation'
                                   )
        WHERE  wn.workflow_id = w.id
      ),
      ARRAY[]::text[]
    ) AS kinds
  FROM  tmpl
  JOIN  lenses.workflows   w ON w.id  = tmpl.workflow_id
  LEFT JOIN lensers.profiles p ON p.id = w.lenser_id
  WHERE w.visibility::text = 'public'
  ORDER BY tmpl.marked_at ASC, w.title ASC
  LIMIT  p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) IS 'Lists curated template workflows that power the "Start from template" strip on WorkflowsPage. A workflow is considered a template when it has a content.tag_map row linking it to the `template` tag.';



CREATE OR REPLACE FUNCTION "public"."fn_list_threads"("p_cursor" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20, "p_tag_slug" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "author_lenser_id" "uuid", "lenser_id" "uuid", "visibility" "text", "title" "text", "content" "text", "lenser_handle" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "reply_count" integer, "view_count" integer, "thumbnail_url" "text", "linked_lens_id" "uuid", "lens_data" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT
    t.id,
    t.lenser_id  AS author_lenser_id,
    t.lenser_id,
    t.visibility::text,
    COALESCE(et.title, 'Untitled') AS title,
    COALESCE(et.content, '')       AS content,
    prof.handle                    AS lenser_handle,
    t.created_at,
    t.updated_at,
    t.reply_count,
    t.view_count,
    t.thumbnail_url,
    t.linked_lens_id,
    t.lens_data
  FROM content.threads t
  LEFT JOIN content.entity_translations et
         ON et.entity_id   = t.id
        AND et.entity_type = 'thread'::content.entity_type_enum
        AND et.is_original = true
  LEFT JOIN lensers.profiles prof ON prof.id = t.lenser_id
  WHERE t.lenser_id = lensers.get_auth_lenser_id()
    AND (
      p_cursor IS NULL
      OR t.created_at < (SELECT ct.created_at FROM content.threads ct WHERE ct.id = p_cursor)
    )
    AND (
      p_tag_slug IS NULL
      OR EXISTS (
        SELECT 1
        FROM content.tag_map tm
        JOIN content.tags tg ON tg.id = tm.tag_id
        WHERE tm.entity_id   = t.id
          AND tm.entity_type = 'thread'::content.entity_type_enum
          AND tg.slug = p_tag_slug
      )
    )
  ORDER BY t.created_at DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") IS 'Lists threads owned by the current authenticated user (all visibilities) with title, content, and lenser_handle pre-joined. Used by threadsRepository.getByLenser(includePrivate=true) when the viewer is the profile owner. Supports cursor pagination and optional tag-slug filtering. Title/content come from entity_translations so private threads hydrate correctly without a second public-view lookup.';



CREATE OR REPLACE FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ta.*)
  FROM agents.tool_assignments ta
  WHERE ta.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    );
$$;


ALTER FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(tp.*)
  FROM agents.tool_profiles tp
  WHERE tp.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY tp.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tools"("p_category" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT lenses.fn_list_tools(p_category);
$$;


ALTER FUNCTION "public"."fn_list_tools"("p_category" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(tr.*)
  FROM agents.tools_registry tr
  WHERE tr.owner_lenser_id = p_owner_lenser_id
    AND (p_owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY tr.name ASC;
$$;


ALTER FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tournaments"("p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT to_jsonb(t.*)
  FROM battles.tournaments t
  WHERE (p_cursor IS NULL OR t.created_at < p_cursor)
  ORDER BY t.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(wa.*)
  FROM agents.workflow_assignments wa
  WHERE wa.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR wa.created_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY wa.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT to_jsonb(wp.*)
  FROM lenses.workflow_phases wp
  JOIN lenses.workflows w ON w.id = wp.workflow_id
  WHERE wp.workflow_id = p_workflow_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wp.ordinal, wp.created_at;
$$;


ALTER FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint DEFAULT 0, "p_limit" integer DEFAULT 200) RETURNS TABLE("event_id" bigint, "type" "text", "run_id" "uuid", "occurred_at" timestamp with time zone, "payload" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    e.event_id,
    e.type,
    e.run_id,
    e.created_at AS occurred_at,
    e.payload
  FROM lenses.workflow_run_events e
  JOIN lenses.workflow_runs r ON r.id = e.run_id
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE e.run_id = p_run_id
    AND e.event_id > COALESCE(p_after_event_id, 0)
    AND (
      r.triggered_by = lensers.get_auth_lenser_id()
      OR w.visibility::text = 'public'
    )
  ORDER BY e.event_id ASC
  LIMIT GREATEST(1, LEAST(COALESCE(p_limit, 200), 1000));
$$;


ALTER FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) IS 'Lists workflow run events after p_after_event_id for SSE replay/catch-up.';



CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "status" "text", "trigger_mode" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone, "global_model_id" "text", "spent_credits" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    r.id,
    r.workflow_id,
    r.status,
    r.trigger_mode,
    r.started_at,
    r.completed_at,
    r.created_at,
    r.global_model_id,
    r.spent_credits
  FROM lenses.workflow_runs r
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE r.workflow_id = p_workflow_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY r.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Owner-only paginated list of workflow runs for a given workflow. Used by the workflow builder history tab.';



CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT to_jsonb(wt.*)
  FROM lenses.workflow_tasks wt
  JOIN lenses.workflows w ON w.id = wt.workflow_id
  WHERE wt.phase_id = p_phase_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wt.ordinal, wt.created_at;
$$;


ALTER FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT to_jsonb(wt.*)
  FROM lenses.workflow_tasks wt
  JOIN lenses.workflows w ON w.id = wt.workflow_id
  WHERE wt.workflow_id = p_workflow_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wt.ordinal, wt.created_at;
$$;


ALTER FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'audit', 'battles'
    AS $$
BEGIN
  INSERT INTO audit.events (
    event_type,
    entity_schema,
    entity_table,
    entity_id,
    actor_type,
    actor_id,
    actor_lenser_id,
    payload,
    severity
  ) VALUES (
    'artifact.' || p_action,
    CASE p_artifact_type
      WHEN 'lens' THEN 'lenses'
      WHEN 'workflow' THEN 'lenses'
      WHEN 'battle' THEN 'battles'
      WHEN 'agent' THEN 'agents'
      ELSE 'public'
    END,
    CASE p_artifact_type
      WHEN 'lens' THEN 'lenses'
      WHEN 'workflow' THEN 'workflows'
      WHEN 'battle' THEN 'battles'
      WHEN 'agent' THEN 'ai_lensers'
      ELSE p_artifact_type
    END,
    p_artifact_id,
    CASE WHEN p_actor_id IS NULL THEN 'system' ELSE 'lenser' END,
    p_actor_id,
    p_actor_id,
    COALESCE(p_payload, '{}'::jsonb),
    'info'
  );
EXCEPTION WHEN undefined_table THEN
  NULL;
END;
$$;


ALTER FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb" DEFAULT NULL::"jsonb", "p_violations" "jsonb" DEFAULT '[]'::"jsonb") RETURNS "battles"."model_test_runs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid UUID := auth.uid();
  v_row battles.model_test_runs%ROWTYPE;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_battle_id IS NULL AND p_template_id IS NULL THEN
    RAISE EXCEPTION 'battle_or_template_required' USING ERRCODE = '22023';
  END IF;

  -- Caller must own the anchor row.
  IF p_battle_id IS NOT NULL
     AND NOT EXISTS (
       SELECT 1 FROM battles.battles
        WHERE id = p_battle_id AND creator_lenser_id = v_uid
     ) THEN
    RAISE EXCEPTION 'battle_not_owned' USING ERRCODE = '42501';
  END IF;
  IF p_template_id IS NOT NULL
     AND NOT EXISTS (
       SELECT 1 FROM battles.templates
        WHERE id = p_template_id AND creator_lenser_id = v_uid
     ) THEN
    RAISE EXCEPTION 'template_not_owned' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.model_test_runs (
    battle_id, template_id, model_provider, model_id,
    prompt_hash, passed, duration_ms, raw_output, violations
  ) VALUES (
    p_battle_id, p_template_id, p_model_provider, p_model_id,
    p_prompt_hash, p_passed, p_duration_ms, p_raw_output,
    COALESCE(p_violations, '[]'::jsonb)
  )
  RETURNING * INTO v_row;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'auth', 'analytics', 'lensers'
    AS $$
DECLARE
  v_user_id   uuid;
  v_lenser_id uuid;
BEGIN
  -- 1. Validation Logic
  IF p_target_type IS NULL THEN
    RAISE EXCEPTION 'target_type is required';
  END IF;

  IF p_path IS NULL THEN
    RAISE EXCEPTION 'path is required';
  END IF;

  -- 2. Identity Resolution
  v_user_id := auth.uid();

  -- Resilience: Verify user exists in auth.users to prevent FK violations from stale JWTs
  -- If user does not exist (e.g., deleted or stale session), fallback to anonymous logging
  IF v_user_id IS NOT NULL THEN
    IF NOT EXISTS (SELECT 1 FROM auth.users WHERE id = v_user_id) THEN
      v_user_id := NULL;
    END IF;
  END IF;

  -- Resolve lenser profile if user is valid
  IF v_user_id IS NOT NULL THEN
    SELECT id
    INTO v_lenser_id
    FROM lensers.profiles
    WHERE user_id = v_user_id;
  END IF;

  -- 3. Debouncing (Avoid duplicate logs within 5 seconds for same user/path)
  IF v_user_id IS NOT NULL THEN
    IF EXISTS (
      SELECT 1
      FROM analytics.page_views
      WHERE user_id = v_user_id
        AND path = p_path
        AND created_at > NOW() - INTERVAL '5 seconds'
    ) THEN
      RETURN;
    END IF;
  END IF;

  -- 4. Audit Log Execution
  INSERT INTO analytics.page_views (
    lenser_id,
    user_id,
    target_type,
    target_id,
    path,
    referrer,
    user_agent,
    client_ip
  )
  VALUES (
    v_lenser_id,
    v_user_id,
    p_target_type,
    p_target_id,
    p_path,
    p_referrer,
    p_user_agent,
    p_client_ip
  );
END;
$$;


ALTER FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_updated INT;
BEGIN
  UPDATE public.notifications
  SET    read_at = now()
  WHERE  id = ANY(p_notification_ids)
  AND    lenser_id = lensers.get_auth_lenser_id()
  AND    read_at IS NULL;

  GET DIAGNOSTICS v_updated = ROW_COUNT;
  RETURN v_updated;
END;
$$;


ALTER FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text" DEFAULT 'tutorial'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'xp', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_row_id    UUID;
  v_inserted  BOOLEAN := false;
BEGIN
  IF p_kind NOT IN ('tutorial', 'walkthrough') THEN
    RAISE EXCEPTION 'invalid_kind'
      USING ERRCODE = '22023',
            DETAIL  = 'kind must be ''tutorial'' or ''walkthrough''',
            HINT    = 'invalid_kind';
  END IF;

  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'Must be authenticated.',
            HINT    = 'permission_denied';
  END IF;

  INSERT INTO lenses.tutorial_completions (lenser_id, tutorial_slug, kind)
  VALUES (v_lenser_id, p_tutorial_slug, p_kind)
  ON CONFLICT (lenser_id, tutorial_slug, kind) DO NOTHING
  RETURNING id INTO v_row_id;

  v_inserted := v_row_id IS NOT NULL;

  RETURN jsonb_build_object(
    'inserted',       v_inserted,
    'tutorial_slug',  p_tutorial_slug,
    'kind',           p_kind
  );
END;
$$;


ALTER FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") IS 'CB-P1b: Marks a tutorial or walkthrough as complete for the calling lenser. Idempotent. Fires XP award trigger on first completion. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text" DEFAULT '_default'::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
DECLARE
    v_owner uuid;
    v_attachment_id uuid;
BEGIN
    SELECT owner_lenser_id INTO v_owner
    FROM media.objects WHERE id = p_object_id;

    IF v_owner IS NULL THEN
        RAISE EXCEPTION 'Media object not found: %', p_object_id;
    END IF;

    IF v_owner <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Permission denied: not object owner';
    END IF;

    INSERT INTO media.attachments (object_id, entity_type, entity_id, binding_key, attached_by)
    VALUES (p_object_id, p_entity_type, p_entity_id, p_binding_key, lensers.get_auth_lenser_id())
    ON CONFLICT (entity_type, entity_id, binding_key)
    DO UPDATE SET object_id = EXCLUDED.object_id, attached_by = EXCLUDED.attached_by, attached_at = now()
    RETURNING id INTO v_attachment_id;

    RETURN v_attachment_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") IS 'Binds a media object to a business entity via a named slot. Upserts: if the (entity_type, entity_id, binding_key) already exists, replaces the bound object.';



CREATE OR REPLACE FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint DEFAULT NULL::bigint, "p_checksum" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
DECLARE
    v_owner uuid;
BEGIN
    SELECT owner_lenser_id INTO v_owner
    FROM media.objects WHERE id = p_object_id;

    IF v_owner IS NULL THEN
        RAISE EXCEPTION 'Media object not found: %', p_object_id;
    END IF;

    IF v_owner <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Permission denied: not object owner';
    END IF;

    UPDATE media.objects
    SET bucket = p_bucket,
        object_key = p_object_key,
        byte_size = COALESCE(p_byte_size, byte_size),
        checksum_sha256 = COALESCE(p_checksum, checksum_sha256),
        lifecycle_state = 'active',
        updated_at = now()
    WHERE id = p_object_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") IS 'Finalizes a media object upload. Sets bucket, object_key, byte_size, checksum, and transitions lifecycle_state to active. Validates ownership.';



CREATE OR REPLACE FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'public'
    AS $$
BEGIN
  UPDATE media.objects
  SET access_count = access_count + 1
  WHERE id = p_object_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") IS 'AT: Increments media.objects.access_count. Called by media-proxy route after successful signed URL generation. Fire-and-forget; errors do not affect the proxy response. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
DECLARE
    v_owner uuid;
BEGIN
    SELECT owner_lenser_id INTO v_owner
    FROM media.objects WHERE id = p_object_id;

    IF v_owner IS NULL THEN
        RAISE EXCEPTION 'Media object not found: %', p_object_id;
    END IF;

    IF v_owner <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Permission denied: not object owner';
    END IF;

    UPDATE media.objects
    SET lifecycle_state = 'deleted', updated_at = now()
    WHERE id = p_object_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text" DEFAULT '_default'::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
BEGIN
    DELETE FROM media.attachments
    WHERE entity_type = p_entity_type
      AND entity_id = p_entity_id
      AND binding_key = p_binding_key
      AND EXISTS (
          SELECT 1 FROM media.objects o
          WHERE o.id = attachments.object_id
            AND o.owner_lenser_id = lensers.get_auth_lenser_id()
      );
END;
$$;


ALTER FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text" DEFAULT NULL::"text", "p_error_msg" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
DECLARE
  v_dlq_id uuid;
BEGIN
  SELECT battles.fn_move_battle_job_to_dlq(p_job_id, p_error_code, p_error_msg)
  INTO v_dlq_id;
  RETURN v_dlq_id;
END;
$$;


ALTER FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_battle        battles.battles;
  v_winner        battles.contenders;
  v_action_url    TEXT;
  v_email_payload JSONB;
  v_voter_ids     UUID[];
  v_contender     RECORD;
  v_outcome_type  TEXT;
  v_body_text     TEXT;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN RETURN; END IF;

  IF v_battle.winner_contender_id IS NOT NULL THEN
    SELECT * INTO v_winner FROM battles.contenders WHERE id = v_battle.winner_contender_id;
    v_body_text := 'Winner: ' || COALESCE(v_winner.display_name, 'Unknown');
  ELSE
    v_body_text := 'The battle ended in a draw.';
  END IF;

  v_action_url := '/battles/' || v_battle.slug || '/result';

  -- ── Contenders: differentiated battle_won / battle_lost ──────────────────
  FOR v_contender IN
    SELECT
      cem.profile_id                                                AS lenser_id,
      c.id                                                          AS contender_id,
      (c.id = v_battle.winner_contender_id)                        AS is_winner
    FROM   battles.contenders c
    JOIN   battles.contender_entity_map cem ON cem.contender_id = c.id
    WHERE  c.battle_id     = p_battle_id
      AND  cem.profile_id  IS NOT NULL
  LOOP
    v_outcome_type := CASE WHEN v_contender.is_winner THEN 'battle_won' ELSE 'battle_lost' END;

    PERFORM public.fn_insert_notification(
      v_contender.lenser_id,
      v_outcome_type,
      'Battle result: ' || v_battle.title,
      v_body_text,
      v_action_url,
      jsonb_build_object(
        'battle_id',     p_battle_id,
        'battle_slug',   v_battle.slug,
        'contender_id',  v_contender.contender_id,
        'winner_id',     v_battle.winner_contender_id,
        'winner_name',   COALESCE(v_winner.display_name, NULL),
        'is_winner',     v_contender.is_winner
      )
      -- p_actor_id = NULL: system outcome, skip block/mute gate
    );
  END LOOP;

  -- ── Voters: keep generic battle_result ───────────────────────────────────
  SELECT array_agg(DISTINCT voter_lenser_id) INTO v_voter_ids
  FROM   battles.votes
  WHERE  battle_id         = p_battle_id
    AND  voter_lenser_id   IS NOT NULL;

  IF v_voter_ids IS NOT NULL AND array_length(v_voter_ids, 1) > 0 THEN
    INSERT INTO public.notifications (lenser_id, type, title, body, action_url, metadata)
    SELECT
      unnest(v_voter_ids),
      'battle_result',
      'Battle result: ' || v_battle.title,
      v_body_text,
      v_action_url,
      jsonb_build_object(
        'battle_id',   p_battle_id,
        'battle_slug', v_battle.slug,
        'winner_id',   v_battle.winner_contender_id,
        'winner_name', COALESCE(v_winner.display_name, NULL)
      )
    ON CONFLICT DO NOTHING;
  END IF;

  -- ── Email edge function (unchanged) ───────────────────────────────────────
  IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net') THEN
    SELECT jsonb_build_object(
      'battle_id',        p_battle_id,
      'battle_title',     v_battle.title,
      'battle_slug',      v_battle.slug,
      'winner_name',      COALESCE(v_winner.display_name, NULL),
      'recipient_emails', (
        SELECT jsonb_agg(email)
        FROM   lensers.profiles
        WHERE  id = ANY(v_voter_ids)
          AND  email IS NOT NULL
      )
    ) INTO v_email_payload;

    PERFORM net.http_post(
      url     := current_setting('app.supabase_url', true) || '/functions/v1/send-battle-result-email',
      headers := jsonb_build_object(
        'Content-Type',  'application/json',
        'Authorization', 'Bearer ' || internal.get_service_role_key()
      ),
      body    := v_email_payload
    );
  END IF;

EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_notify_battle_result failed for battle %: %', p_battle_id, SQLERRM;
END;
$$;


ALTER FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'vault'
    AS $$
DECLARE
  v_conn          public.user_oauth_connections%ROWTYPE;
  v_refresh_token text;
BEGIN
  SELECT * INTO v_conn
  FROM public.user_oauth_connections
  WHERE lenser_id = p_lenser_id
    AND ref       = p_ref
    AND (p_workspace_id IS NULL OR workspace_id = p_workspace_id)
    AND is_active = true
    AND revoked_at IS NULL;

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  -- No refresh token stored — caller cannot refresh
  IF v_conn.refresh_token_id IS NULL THEN
    RETURN NULL;
  END IF;

  -- Not near expiry (> 5 min remaining) — no refresh needed
  IF v_conn.expires_at IS NOT NULL
     AND v_conn.expires_at > (now() + interval '5 minutes')
  THEN
    RETURN NULL;
  END IF;

  SELECT decrypted_secret INTO v_refresh_token
  FROM vault.decrypted_secrets
  WHERE id = v_conn.refresh_token_id;

  IF v_refresh_token IS NULL THEN
    RETURN NULL;
  END IF;

  RETURN jsonb_build_object(
    'connection_id',   v_conn.id,
    'workspace_id',    v_conn.workspace_id,
    'refresh_token',   v_refresh_token,
    'expires_at',      v_conn.expires_at,
    'granted_scopes',  v_conn.granted_scopes,
    'provider',        v_conn.provider,
    'capability',      v_conn.capability
  );
END;
$$;


ALTER FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_list_connections"() RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "provider" "text", "capability" "text", "connection_label" "text", "ref" "text", "granted_scopes" "text"[], "expires_at" timestamp with time zone, "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT
    c.id,
    c.workspace_id,
    c.provider,
    c.capability,
    c.connection_label,
    c.ref,
    c.granted_scopes,
    c.expires_at,
    c.is_active,
    c.created_at,
    c.updated_at
  FROM public.user_oauth_connections c
  JOIN lensers.profiles p ON p.id = c.lenser_id
  WHERE p.user_id = auth.uid()
    AND c.revoked_at IS NULL
  ORDER BY c.provider, c.capability, c.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_oauth_list_connections"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[] DEFAULT '{}'::"text"[], "p_workspace_id" "uuid" DEFAULT NULL::"uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'vault'
    AS $$
DECLARE
  v_conn         public.user_oauth_connections%ROWTYPE;
  v_access_token text;
BEGIN
  SELECT * INTO v_conn
  FROM public.user_oauth_connections
  WHERE lenser_id = p_lenser_id
    AND ref       = p_ref
    AND (p_workspace_id IS NULL OR workspace_id = p_workspace_id)
    AND is_active = true
    AND revoked_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'oauth_resolve_not_found: ref=% lenser=%', p_ref, p_lenser_id
      USING ERRCODE = 'P0002';
  END IF;

  -- Scope containment check: required ⊆ granted
  IF array_length(p_required_scopes, 1) > 0 THEN
    IF NOT (p_required_scopes <@ v_conn.granted_scopes) THEN
      RAISE EXCEPTION
        'oauth_resolve_scope_mismatch: ref=%, required=%, granted=%',
        p_ref, p_required_scopes, v_conn.granted_scopes
        USING ERRCODE = '42501';
    END IF;
  END IF;

  -- Retrieve access token from vault
  SELECT decrypted_secret INTO v_access_token
  FROM vault.decrypted_secrets
  WHERE id = v_conn.access_token_id;

  IF v_access_token IS NULL THEN
    RAISE EXCEPTION 'oauth_resolve_vault_miss: vault secret missing for ref=%', p_ref
      USING ERRCODE = 'P0001';
  END IF;

  RETURN v_access_token;
END;
$$;


ALTER FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'oauth_revoke_unauthorized: no profile for caller'
      USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM public.user_oauth_connections
    WHERE id = p_connection_id
      AND lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'oauth_revoke_forbidden: connection not found or not owned'
      USING ERRCODE = '42501';
  END IF;

  UPDATE public.user_oauth_connections
  SET
    is_active  = false,
    revoked_at = now(),
    updated_at = now()
  WHERE id = p_connection_id
    AND lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text" DEFAULT NULL::"text", "p_granted_scopes" "text"[] DEFAULT '{}'::"text"[], "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'vault'
    AS $$
DECLARE
  v_ref            text;
  v_access_name    text;
  v_refresh_name   text;
  v_access_id      uuid;
  v_refresh_id     uuid;
  v_conn_id        uuid;
BEGIN
  v_ref          := p_provider || '.' || p_capability || '.' || p_label;
  v_access_name  := 'oauth_access_'  || p_lenser_id::text || '_' || v_ref;
  v_refresh_name := 'oauth_refresh_' || p_lenser_id::text || '_' || v_ref;

  -- Store or rotate access token in vault
  SELECT id INTO v_access_id
  FROM vault.secrets
  WHERE name = v_access_name
  LIMIT 1;

  IF v_access_id IS NULL THEN
    INSERT INTO vault.secrets (name, secret, description)
    VALUES (v_access_name, p_access_token, 'OAuth access token: ' || v_ref)
    RETURNING id INTO v_access_id;
  ELSE
    UPDATE vault.secrets
    SET secret = p_access_token
    WHERE id = v_access_id;
  END IF;

  -- Store or rotate refresh token in vault (if provided)
  IF p_refresh_token IS NOT NULL THEN
    SELECT id INTO v_refresh_id
    FROM vault.secrets
    WHERE name = v_refresh_name
    LIMIT 1;

    IF v_refresh_id IS NULL THEN
      INSERT INTO vault.secrets (name, secret, description)
      VALUES (v_refresh_name, p_refresh_token, 'OAuth refresh token: ' || v_ref)
      RETURNING id INTO v_refresh_id;
    ELSE
      UPDATE vault.secrets
      SET secret = p_refresh_token
      WHERE id = v_refresh_id;
    END IF;
  END IF;

  -- Upsert the connection row (on conflict: update tokens + scopes + expiry)
  INSERT INTO public.user_oauth_connections (
    lenser_id,
    workspace_id,
    provider,
    capability,
    connection_label,
    ref,
    access_token_id,
    refresh_token_id,
    granted_scopes,
    expires_at,
    is_active,
    revoked_at,
    updated_at
  )
  VALUES (
    p_lenser_id,
    p_workspace_id,
    p_provider,
    p_capability,
    p_label,
    v_ref,
    v_access_id,
    v_refresh_id,
    p_granted_scopes,
    p_expires_at,
    true,
    NULL,
    now()
  )
  ON CONFLICT (lenser_id, workspace_id, ref) DO UPDATE
    SET access_token_id  = EXCLUDED.access_token_id,
        -- Only update refresh_token_id if a new one was provided
        refresh_token_id = COALESCE(
          CASE WHEN p_refresh_token IS NOT NULL THEN EXCLUDED.refresh_token_id END,
          user_oauth_connections.refresh_token_id
        ),
        granted_scopes   = EXCLUDED.granted_scopes,
        expires_at       = EXCLUDED.expires_at,
        is_active        = true,
        revoked_at       = NULL,
        updated_at       = now()
  RETURNING id INTO v_conn_id;

  RETURN v_conn_id;
END;
$$;


ALTER FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean DEFAULT false, "p_ip_address" "text" DEFAULT NULL::"text", "p_user_agent" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  -- Basic sanity guards
  IF trim(p_name) = '' OR trim(p_email) = '' OR trim(p_message) = '' THEN
    RAISE EXCEPTION 'name, email, and message are required' USING ERRCODE = '22023';
  END IF;

  IF NOT p_kvkk_approved THEN
    RAISE EXCEPTION 'KVKK consent is required' USING ERRCODE = '22023';
  END IF;

  -- Resolve optional lenser_id from auth context
  IF auth.uid() IS NOT NULL THEN
    SELECT id INTO v_lenser_id
    FROM lensers.profiles
    WHERE user_id = auth.uid()
    LIMIT 1;
  END IF;

  INSERT INTO analytics.contact (
    lenser_id,
    name,
    email,
    subject,
    message,
    kvkk_approved,
    ip_address,
    user_agent
  ) VALUES (
    v_lenser_id,
    trim(p_name),
    lower(trim(p_email)),
    trim(p_subject),
    trim(p_message),
    p_kvkk_approved,
    p_ip_address::inet,
    p_user_agent
  );
END;
$$;


ALTER FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") IS 'Inserts a contact form submission into ops.contact. Accessible to anonymous users.';



CREATE OR REPLACE FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, agent_paused)
  VALUES (p_ai_lenser_id, true)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET agent_paused = true,
        updated_at   = now();

  INSERT INTO agents.policy_evaluations (
    ai_lenser_id,
    evaluation_point,
    policy_type,
    verdict,
    reason,
    context,
    evaluated_at
  ) VALUES (
    p_ai_lenser_id,
    'pre_run',
    'pause',
    'deny',
    'agent paused by owner',
    jsonb_build_object('paused_by', auth.uid()),
    now()
  );
END;
$$;


ALTER FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "lenser_id" "uuid", "body" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO battles.comments (battle_id, lenser_id, body)
  VALUES (p_battle_id, v_lenser_id, p_body)
  RETURNING id, battle_id, lenser_id, body, created_at, updated_at;
END;
$$;


ALTER FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text" DEFAULT 'lenser'::"text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "sender_id" "uuid", "sender_handle" "text", "sender_role" "text", "body" "text", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_sender_id uuid;
BEGIN
  v_sender_id := lensers.get_auth_lenser_id();
  IF v_sender_id IS NULL THEN
    RAISE EXCEPTION 'Unauthorized' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO battles.global_messages (battle_id, sender_id, sender_handle, sender_role, body)
  VALUES (p_battle_id, v_sender_id, p_sender_handle, p_sender_role, p_body)
  RETURNING
    battles.global_messages.id,
    battles.global_messages.battle_id,
    battles.global_messages.sender_id,
    battles.global_messages.sender_handle,
    battles.global_messages.sender_role,
    battles.global_messages.body,
    battles.global_messages.created_at;
END;
$$;


ALTER FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") IS 'Posts a chat message. sender_id is resolved from JWT — not accepted from caller.';



CREATE OR REPLACE FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'xp', 'battles', 'public'
    AS $$
DECLARE
  v_score     integer := 0;
  v_bio       text;
  v_avatar    text;
  v_location  text;
  v_website   text;
  v_battles   bigint;
  v_total_xp  bigint;
BEGIN
  SELECT
    p.bio,
    p.avatar_url,
    p.location,
    p.website_url
  INTO v_bio, v_avatar, v_location, v_website
  FROM lensers.profiles p
  WHERE p.id = p_lenser_id;

  IF NOT FOUND THEN
    RETURN 0;
  END IF;

  IF v_bio IS NOT NULL AND length(trim(v_bio)) > 0 THEN
    v_score := v_score + 25;
  END IF;
  IF v_avatar IS NOT NULL AND length(trim(v_avatar)) > 0 THEN
    v_score := v_score + 25;
  END IF;
  IF v_location IS NOT NULL AND length(trim(v_location)) > 0 THEN
    v_score := v_score + 15;
  END IF;
  IF v_website IS NOT NULL AND length(trim(v_website)) > 0 THEN
    v_score := v_score + 15;
  END IF;

  -- Battle participation ('cancelled' is not a valid battles.battle_status_enum value)
  SELECT COUNT(*) INTO v_battles
  FROM battles.contenders bc
  JOIN battles.battles b ON b.id = bc.battle_id
  WHERE bc.contender_ref_id = p_lenser_id
    AND b.status <> 'draft'::battles.battle_status_enum;

  IF v_battles > 0 THEN
    v_score := v_score + 10;
  END IF;

  -- XP earned (any positive amount)
  SELECT COALESCE(SUM(xp), 0) INTO v_total_xp
  FROM xp.events
  WHERE lenser_id = p_lenser_id;

  IF v_total_xp > 0 THEN
    v_score := v_score + 10;
  END IF;

  RETURN LEAST(v_score, 100);
END;
$$;


ALTER FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") IS 'Returns a 0–100 profile completion score. Breakdown: bio +25, avatar +25, location +15, website +15, battles >0 +10, XP >0 +10.';



CREATE TABLE IF NOT EXISTS "agents"."memory_profiles" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "scope_type" "text" DEFAULT 'project'::"text" NOT NULL,
    "isolation_mode" "text" DEFAULT 'isolated'::"text" NOT NULL,
    "retention_days" integer DEFAULT 30 NOT NULL,
    "visibility" "text" DEFAULT 'private'::"text" NOT NULL,
    "summary_strategy" "text" DEFAULT 'rolling_summary'::"text" NOT NULL,
    "reset_policy" "text" DEFAULT 'manual'::"text" NOT NULL,
    "is_default" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "seed" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    CONSTRAINT "memory_profiles_retention_days_check" CHECK (("retention_days" >= 0))
);


ALTER TABLE "agents"."memory_profiles" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") RETURNS "agents"."memory_profiles"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run     agents.scratchpad_runs;
  v_profile agents.memory_profiles;
BEGIN
  SELECT * INTO v_run FROM agents.scratchpad_runs WHERE id = p_run_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'scratchpad run not found' USING ERRCODE = 'P0002';
  END IF;

  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_memory_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_run.ai_lenser_id)
     OR NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.memory_profiles
  SET seed = seed || jsonb_build_array(jsonb_build_object(
        'source', 'scratchpad',
        'run_id', v_run.id,
        'prompt', v_run.prompt,
        'output', v_run.output,
        'promoted_at', now()
      )),
      updated_at = now()
  WHERE id = p_memory_profile_id
  RETURNING * INTO v_profile;

  RETURN v_profile;
END;
$$;


ALTER FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'extensions', 'public'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_hash TEXT;
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM battles.battles
    WHERE id = p_battle_id
      AND creator_lenser_id = v_lenser_id
      AND status = 'draft'::battles.battle_status_enum
      AND deleted_at IS NULL
  ) THEN
    RAISE EXCEPTION 'not_authorized_or_invalid_state' USING ERRCODE = '42501';
  END IF;

  v_hash := public.fn_create_battle_config_snapshot(p_battle_id, 'publish');

  UPDATE battles.battles
     SET status = 'open'::battles.battle_status_enum,
         published_at = now(),
         updated_at = now()
   WHERE id = p_battle_id;

  INSERT INTO audit.attestations (
    entity_type, entity_id, attested_by, attestation_type, proof_payload
  )
  VALUES (
    'battle', p_battle_id, v_lenser_id, 'battle_published',
    jsonb_build_object('snapshot_hash', v_hash, 'battle_id', p_battle_id)
  );

  INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
  VALUES (
    p_battle_id,
    'published',
    v_lenser_id,
    jsonb_build_object('snapshot_hash', v_hash)
  );

  RETURN jsonb_build_object(
    'snapshot_hash', v_hash,
    'status', 'open',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") IS 'Atomically publishes a battle: freezes rules into rule_snapshots, transitions status to ''open'', and records an audit attestation. Phase A fix: search_path now includes extensions for pgcrypto digest(). Only the battle creator may call this. Battle must be in ''draft'' status. Direct UPDATE on battles.battles.status is restricted to service_role via column-level revoke (see A3 below).';



CREATE OR REPLACE FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  PERFORM lenses.fn_publish_workflow_version(p_version_id);
END;
$$;


ALTER FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") IS 'Public wrapper: transitions a draft version to published and sets head_version_id. Owner-only. Delegates auth + immutability guard to lenses.fn_publish_workflow_version.';



CREATE OR REPLACE FUNCTION "public"."fn_purge_due_accounts"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
DECLARE
  v_count   int := 0;
  v_profile RECORD;
BEGIN
  FOR v_profile IN
    SELECT id, user_id, deletion_deadline_at
    FROM lensers.profiles
    WHERE status = 'pending_deletion'
      AND deletion_deadline_at <= now() - interval '1 hour'
  LOOP
    UPDATE lensers.profiles
    SET display_name = 'Deleted User',
        handle       = 'deleted_' || v_profile.id::text,
        bio          = null,
        headline     = null,
        avatar_url   = null,
        banner_url   = null,
        website_url  = null,
        preferences  = null,
        status       = 'deleted',
        updated_at   = now()
    WHERE id = v_profile.id;

    PERFORM lensers.log_account_lifecycle_event(
      v_profile.id,
      v_profile.user_id,
      'deletion_completed',
      'pending_deletion'::lensers.lenser_status,
      'deleted'::lensers.lenser_status,
      'system_job',
      jsonb_build_object(
        'deletion_deadline_at',     v_profile.deletion_deadline_at,
        'purged_after_grace_period', true
      )
    );

    DELETE FROM lensers.relationships
    WHERE source_profile_id = v_profile.id OR target_profile_id = v_profile.id;

    DELETE FROM lensers.social_links WHERE lenser_id = v_profile.id;

    v_count := v_count + 1;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_purge_due_accounts"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_queue_freeze"("p_reason" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_profile_id UUID;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'queue_freeze_forbidden: admin role required';
  END IF;

  SELECT id INTO v_profile_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  UPDATE admin.execution_control
  SET    queue_frozen  = true,
         frozen_reason = p_reason,
         frozen_by     = v_profile_id,
         frozen_at     = now()
  WHERE  id = 1;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'queue.frozen',
    'lenser',
    v_profile_id,
    'warn',
    jsonb_build_object('reason', p_reason)
  );
END;
$$;


ALTER FUNCTION "public"."fn_queue_freeze"("p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") IS 'Admin-only: freeze the scheduled-workflow dispatch queue. Prevents fn_dispatch_scheduled_workflows from enqueuing new runs without cancelling runs already in flight. Resume with fn_queue_unfreeze.';



CREATE OR REPLACE FUNCTION "public"."fn_queue_unfreeze"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_profile_id UUID;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'queue_unfreeze_forbidden: admin role required';
  END IF;

  SELECT id INTO v_profile_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  UPDATE admin.execution_control
  SET    queue_frozen  = false,
         frozen_reason = NULL,
         frozen_by     = NULL,
         frozen_at     = NULL
  WHERE  id = 1;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'queue.unfrozen',
    'lenser',
    v_profile_id,
    'info',
    jsonb_build_object('resumed_by', v_profile_id)
  );
END;
$$;


ALTER FUNCTION "public"."fn_queue_unfreeze"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_queue_unfreeze"() IS 'Admin-only: resume the scheduled-workflow dispatch queue after a freeze.';



CREATE TABLE IF NOT EXISTS "agents"."memories" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "profile_id" "uuid" NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "scope" "text" NOT NULL,
    "source" "text" NOT NULL,
    "content" "text" NOT NULL,
    "embedding_metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "confidence" numeric DEFAULT 0.5 NOT NULL,
    "expires_at" timestamp with time zone,
    "team_run_id" "uuid",
    "is_redacted" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_agents_memories_content_len" CHECK ((("content" IS NULL) OR ("length"("content") <= 20000))),
    CONSTRAINT "memories_confidence_check" CHECK ((("confidence" >= (0)::numeric) AND ("confidence" <= (1)::numeric))),
    CONSTRAINT "memories_scope_check" CHECK (("scope" = ANY (ARRAY['project'::"text", 'conversation'::"text", 'global'::"text"]))),
    CONSTRAINT "memories_source_check" CHECK (("source" = ANY (ARRAY['user'::"text", 'agent'::"text", 'tool'::"text", 'eval'::"text", 'manual'::"text"])))
);


ALTER TABLE "agents"."memories" OWNER TO "postgres";


COMMENT ON TABLE "agents"."memories" IS 'Per-agent memory entries. Profile carries config; entries carry content. Writes are gated to successful runs; reads are logged.';



CREATE OR REPLACE FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 10, "p_team_run_id" "uuid" DEFAULT NULL::"uuid") RETURNS SETOF "agents"."memories"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_profile agents.memory_profiles;
  v_row     agents.memories;
BEGIN
  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  FOR v_row IN
    SELECT *
    FROM agents.memories
    WHERE profile_id = p_profile_id
      AND is_redacted = false
      AND (p_scope IS NULL OR scope = p_scope)
      AND (expires_at IS NULL OR expires_at > now())
    ORDER BY confidence DESC, created_at DESC
    LIMIT GREATEST(COALESCE(p_limit, 10), 1)
  LOOP
    INSERT INTO agents.memory_access_logs (memory_id, team_run_id, action, context)
    VALUES (v_row.id, p_team_run_id, 'read',
            jsonb_build_object('scope', p_scope, 'profile_id', p_profile_id));
    RETURN NEXT v_row;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_report    agents.run_reports;
  v_id        uuid;
BEGIN
  SELECT * INTO v_report
    FROM agents.run_reports
   WHERE id = p_run_report_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'run report not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_report.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF p_incident_type NOT IN (
    'tool_failure','budget_exceeded','policy_violation','timeout',
    'step_failure','approval_rejected','kill_switch'
  ) THEN
    RAISE EXCEPTION 'invalid incident_type: %', p_incident_type USING ERRCODE = '22023';
  END IF;

  IF p_severity NOT IN ('low','medium','high','critical') THEN
    RAISE EXCEPTION 'invalid severity: %', p_severity USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.run_incidents (
    run_report_id,
    ai_lenser_id,
    incident_type,
    severity,
    title,
    description,
    context
  ) VALUES (
    p_run_report_id,
    v_report.ai_lenser_id,
    p_incident_type,
    p_severity,
    p_title,
    p_description,
    COALESCE(p_context, '{}'::jsonb)
  )
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
  v_id        uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_target_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot record provenance for run %', p_target_run_id
      USING ERRCODE = '42501';
  END IF;

  INSERT INTO lenses.workflow_run_provenance (
    source_run_id, source_node_id, source_output_path,
    target_run_id, target_node_id, target_input_path,
    transform
  )
  VALUES (
    p_source_run_id, p_source_node_id, p_source_output_path,
    p_target_run_id, p_target_node_id, p_target_input_path,
    p_transform
  )
  ON CONFLICT (source_run_id, source_node_id, source_output_path,
               target_run_id, target_node_id, target_input_path)
  DO UPDATE SET transform = COALESCE(EXCLUDED.transform, workflow_run_provenance.transform)
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") IS 'Records a field-level data handoff. Idempotent on the (source, target, path) tuple. Transform JSON may carry mapping expression captured at edge-binding time.';



CREATE OR REPLACE FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  IF p_battle_id IS NULL THEN
    RAISE EXCEPTION 'battle_id required' USING ERRCODE = '22023';
  END IF;
  IF p_plugin_id IS NULL OR length(trim(p_plugin_id)) = 0 THEN
    RAISE EXCEPTION 'plugin_id required' USING ERRCODE = '22023';
  END IF;

  INSERT INTO battles.scoring_plugin_signals (battle_id, plugin_id, signals, recorded_at)
  VALUES (p_battle_id, p_plugin_id, COALESCE(p_signals, '{}'::jsonb), now())
  ON CONFLICT (battle_id, plugin_id) DO UPDATE
    SET signals     = EXCLUDED.signals,
        recorded_at = EXCLUDED.recorded_at;
END;
$$;


ALTER FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") IS 'Phase R2: upserts a (battle_id, plugin_id) row in battles.scoring_plugin_signals. Service-role only — plugins run server side under service_role. battles.fn_battles_finalize is intentionally left untouched in this migration.';



CREATE OR REPLACE FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text" DEFAULT NULL::"text", "p_lens_hash" "text" DEFAULT NULL::"text", "p_agent_config_hash" "text" DEFAULT NULL::"text", "p_runner_version" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_policy_passed" boolean DEFAULT false) RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
  SELECT execution.fn_record_signed_attestation(
    p_run_id, p_envelope_kid, p_envelope_iat, p_envelope_nonce,
    p_canonical_jcs_b64url, p_signature_b64url,
    p_workflow_hash, p_lens_hash, p_agent_config_hash,
    p_runner_version, p_cli_version, p_policy_passed
  );
$$;


ALTER FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_mem agents.memories;
BEGIN
  SELECT * INTO v_mem FROM agents.memories WHERE id = p_memory_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_mem.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.memories
     SET content = '[redacted]',
         is_redacted = true
   WHERE id = p_memory_id;

  INSERT INTO agents.memory_access_logs (memory_id, action, context)
  VALUES (p_memory_id, 'redact', jsonb_build_object('reason', COALESCE(p_reason, '')));
END;
$$;


ALTER FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses', 'extensions'
    AS $$
DECLARE
  v_snapshot JSONB;
BEGIN
  SELECT jsonb_build_object(
    'agent_id', al.id,
    'profile_id', al.profile_id,
    'handle', p.handle,
    'display_name', p.display_name,
    'runtime_pref', al.runtime_pref,
    'is_active', al.is_active,
    'suspended', al.suspended_at IS NOT NULL,
    'personality_note_hash', CASE
      WHEN al.personality_note IS NULL THEN NULL
      ELSE encode(extensions.digest(al.personality_note, 'sha256'), 'hex')
    END,
    'policies', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', pol.id,
        'can_join_battles', pol.can_join_battles,
        'can_vote', pol.can_vote,
        'can_create_battles', pol.can_create_battles,
        'can_receive_sponsorship', pol.can_receive_sponsorship,
        'model_binding_mode', pol.model_binding_mode,
        'max_daily_battles', pol.max_daily_battles,
        'max_daily_votes', pol.max_daily_votes,
        'allowed_battle_types', pol.allowed_battle_types,
        'spending_limit_credits', pol.spending_limit_credits,
        'allowed_output_modalities', pol.allowed_output_modalities,
        'is_public_policy', pol.is_public_policy
      ) ORDER BY pol.created_at)
      FROM agents.policies pol
      WHERE pol.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'lens_bindings', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', lb.id,
        'lens_id', lb.lens_id,
        'version_id', COALESCE(lb.version_id, l.head_version_id),
        'is_default', lb.is_default,
        'category_tags', lb.category_tags
      ) ORDER BY lb.created_at)
      FROM agents.lens_bindings lb
      JOIN lenses.lenses l ON l.id = lb.lens_id
      WHERE lb.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'model_bindings', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', mb.id,
        'model_id', mb.model_id,
        'is_default', mb.is_default,
        'category_tags', mb.category_tags,
        'byok_adapter', mb.byok_adapter,
        'ollama_endpoint_hash', CASE
          WHEN mb.ollama_endpoint IS NULL THEN NULL
          ELSE encode(extensions.digest(mb.ollama_endpoint, 'sha256'), 'hex')
        END
      ) ORDER BY mb.created_at)
      FROM agents.model_bindings mb
      WHERE mb.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'tool_profiles', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', tp.id,
        'allow_tools', tp.allow_tools,
        'deny_tools', tp.deny_tools,
        'tool_groups', tp.tool_groups,
        'requires_approval', tp.requires_approval,
        'is_default', tp.is_default
      ) ORDER BY tp.created_at)
      FROM agents.tool_profiles tp
      WHERE tp.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'tool_assignments', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', ta.id,
        'tool_id', ta.tool_id,
        'profile_id', ta.profile_id,
        'allowed', ta.allowed
      ) ORDER BY ta.created_at)
      FROM agents.tool_assignments ta
      WHERE ta.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'memory_policy', jsonb_build_object(
      'memory_profile_count', (
        SELECT count(*) FROM agents.memory_profiles mp WHERE mp.ai_lenser_id = al.id
      ),
      'memory_body_included', false
    ),
    'redaction', jsonb_build_object(
      'secrets_included', false,
      'oauth_tokens_included', false,
      'api_keys_included', false,
      'private_memory_bodies_included', false
    )
  )
  INTO v_snapshot
  FROM agents.ai_lensers al
  JOIN lensers.profiles p ON p.id = al.profile_id
  WHERE al.id = p_ai_lenser_id;

  IF v_snapshot IS NULL THEN
    RAISE EXCEPTION 'agent_not_found: %', p_ai_lenser_id USING ERRCODE = 'P0002';
  END IF;

  RETURN v_snapshot;
END;
$$;


ALTER FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") RETURNS "text"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'extensions'
    AS $$
  SELECT encode(extensions.digest(public.fn_redacted_agent_snapshot(p_ai_lenser_id)::text, 'sha256'), 'hex');
$$;


ALTER FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."tools_registry" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "owner_lenser_id" "uuid" NOT NULL,
    "key" "text" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "category" "text" DEFAULT 'general'::"text" NOT NULL,
    "schema_input" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "schema_output" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "auth_method" "text" DEFAULT 'none'::"text" NOT NULL,
    "requires_approval" boolean DEFAULT false NOT NULL,
    "is_dangerous" boolean DEFAULT false NOT NULL,
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "egress_class" "text" DEFAULT 'none'::"text" NOT NULL,
    CONSTRAINT "tools_registry_auth_method_check" CHECK (("auth_method" = ANY (ARRAY['none'::"text", 'api_key'::"text", 'oauth'::"text", 'service_account'::"text"]))),
    CONSTRAINT "tools_registry_egress_class_check" CHECK (("egress_class" = ANY (ARRAY['none'::"text", 'read_only'::"text", 'write'::"text"]))),
    CONSTRAINT "tools_registry_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'disabled'::"text", 'deprecated'::"text"])))
);


ALTER TABLE "agents"."tools_registry" OWNER TO "postgres";


COMMENT ON TABLE "agents"."tools_registry" IS 'Per-owner registry of tool definitions. Tool assignments bind a registered tool to an agent.';



COMMENT ON COLUMN "agents"."tools_registry"."egress_class" IS 'Sandbox classification: none (compute-only), read_only (outbound reads), write (mutations). write auto-forces requires_approval.';



CREATE OR REPLACE FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text" DEFAULT NULL::"text", "p_category" "text" DEFAULT 'general'::"text", "p_schema_input" "jsonb" DEFAULT '{}'::"jsonb", "p_schema_output" "jsonb" DEFAULT '{}'::"jsonb", "p_auth_method" "text" DEFAULT 'none'::"text", "p_requires_approval" boolean DEFAULT false, "p_is_dangerous" boolean DEFAULT false) RETURNS "agents"."tools_registry"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_owner uuid := lensers.get_auth_human_lenser_id();
  v_row   agents.tools_registry;
BEGIN
  IF v_owner IS NULL THEN
    RAISE EXCEPTION 'authentication required or no human lenser profile found'
      USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.tools_registry (
    owner_lenser_id, key, name, description, category,
    schema_input, schema_output, auth_method, requires_approval, is_dangerous
  )
  VALUES (
    v_owner, p_key, p_name, p_description, COALESCE(p_category, 'general'),
    COALESCE(p_schema_input,      '{}'::jsonb),
    COALESCE(p_schema_output,     '{}'::jsonb),
    COALESCE(p_auth_method,       'none'),
    COALESCE(p_requires_approval, false),
    COALESCE(p_is_dangerous,      false)
  )
  ON CONFLICT (owner_lenser_id, key)
  DO UPDATE SET
    name              = EXCLUDED.name,
    description       = EXCLUDED.description,
    category          = EXCLUDED.category,
    schema_input      = EXCLUDED.schema_input,
    schema_output     = EXCLUDED.schema_output,
    auth_method       = EXCLUDED.auth_method,
    requires_approval = EXCLUDED.requires_approval,
    is_dangerous      = EXCLUDED.is_dangerous,
    updated_at        = now()
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."tournament_contenders" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "tournament_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "seed" integer,
    "status" "text" DEFAULT 'registered'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tournament_contenders_status_check" CHECK (("status" = ANY (ARRAY['registered'::"text", 'active'::"text", 'eliminated'::"text", 'winner'::"text"])))
);


ALTER TABLE "battles"."tournament_contenders" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS "battles"."tournament_contenders"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_row       battles.tournament_contenders;
  v_count     INT;
  v_max       INT;
BEGIN
  v_lenser_id := COALESCE(p_lenser_id, auth.uid());

  SELECT max_contenders INTO v_max FROM battles.tournaments WHERE id = p_tournament_id;
  SELECT COUNT(*) INTO v_count FROM battles.tournament_contenders WHERE tournament_id = p_tournament_id;

  IF v_count >= v_max THEN
    RAISE EXCEPTION 'Tournament is full (% max contenders)', v_max;
  END IF;

  INSERT INTO battles.tournament_contenders (tournament_id, lenser_id)
  VALUES (p_tournament_id, v_lenser_id)
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_target_id uuid;
BEGIN
  SELECT id INTO v_target_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_target_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.relationships
  SET status = 'rejected',
      responded_at = now()
  WHERE source_profile_id = p_source_profile_id
    AND target_profile_id = v_target_id
    AND status = 'pending';

  IF NOT FOUND THEN
    RETURN jsonb_build_object('success', false, 'reason', 'no_pending_request');
  END IF;

  RETURN jsonb_build_object('success', true);
END;
$$;


ALTER FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_inv agents.tool_invocations;
BEGIN
  SELECT * INTO v_inv FROM agents.tool_invocations WHERE id = p_invocation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'invocation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_inv.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF v_inv.approval_status <> 'pending' THEN
    RAISE EXCEPTION 'invocation is not pending approval' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.tool_invocations
     SET approval_status     = 'rejected',
         approval_decided_by = auth.uid(),
         approval_reason     = p_reason,
         status              = 'rejected',
         completed_at        = now()
   WHERE id = p_invocation_id;
END;
$$;


ALTER FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM content.threads
    WHERE id = p_thread_id AND lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RETURN;
  END IF;

  DELETE FROM content.tag_map
  WHERE entity_type::text = 'thread' AND entity_id = p_thread_id;

  IF p_tag_ids IS NOT NULL AND array_length(p_tag_ids, 1) > 0 THEN
    INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
    SELECT 'thread', p_thread_id, unnest(p_tag_ids)
    ON CONFLICT DO NOTHING;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) IS 'Security wrapper: atomically replace all tag mappings for a thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  DELETE FROM battles.contenders c
  USING battles.battles b
  WHERE c.id = p_contender_id
    AND c.battle_id = b.id
    AND b.creator_lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id uuid;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.relationships
  SET status = 'removed',
      removed_at = now()
  WHERE source_profile_id = v_source_id
    AND target_profile_id = p_target_profile_id
    AND status IN ('accepted', 'pending');

  RETURN jsonb_build_object('following', false);
END;
$$;


ALTER FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_id   uuid;
  v_idx  integer := 0;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'reorder_phases_forbidden' USING ERRCODE = '42501';
  END IF;

  FOREACH v_id IN ARRAY p_ordered_ids LOOP
    UPDATE lenses.workflow_phases SET ordinal = v_idx WHERE id = v_id AND workflow_id = p_workflow_id;
    v_idx := v_idx + 1;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_id   uuid;
  v_idx  integer := 0;
BEGIN
  -- Verify ownership via phase → workflow
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflow_phases wp
    JOIN lenses.workflows w ON w.id = wp.workflow_id
    WHERE wp.id = p_phase_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'reorder_tasks_forbidden' USING ERRCODE = '42501';
  END IF;

  FOREACH v_id IN ARRAY p_ordered_ids LOOP
    UPDATE lenses.workflow_tasks SET ordinal = v_idx WHERE id = v_id AND phase_id = p_phase_id;
    v_idx := v_idx + 1;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id     uuid;
  v_target        RECORD;
  v_existing      lensers.relationship_status;
  v_new_status    lensers.relationship_status;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  IF v_source_id = p_target_profile_id THEN
    RAISE EXCEPTION 'Cannot follow yourself';
  END IF;

  -- Check target exists and is active
  SELECT p.id, p.status, p.visibility INTO v_target
  FROM lensers.profiles p WHERE p.id = p_target_profile_id;

  IF NOT FOUND OR v_target.status NOT IN ('active') THEN
    RETURN jsonb_build_object('status', 'rejected', 'reason', 'target_unavailable');
  END IF;

  -- Check if blocked in either direction
  IF EXISTS (
    SELECT 1 FROM lensers.relationships
    WHERE status = 'blocked'
      AND (
        (source_profile_id = v_source_id AND target_profile_id = p_target_profile_id)
        OR (source_profile_id = p_target_profile_id AND target_profile_id = v_source_id)
      )
  ) THEN
    RETURN jsonb_build_object('status', 'rejected', 'reason', 'blocked');
  END IF;

  -- Check existing relationship
  SELECT r.status INTO v_existing
  FROM lensers.relationships r
  WHERE r.source_profile_id = v_source_id AND r.target_profile_id = p_target_profile_id;

  IF v_existing = 'accepted' THEN
    RETURN jsonb_build_object('status', 'accepted', 'reason', 'already_following');
  END IF;

  IF v_existing = 'pending' THEN
    RETURN jsonb_build_object('status', 'pending', 'reason', 'already_pending');
  END IF;

  -- Determine status based on target visibility
  IF v_target.visibility IN ('public', 'community') THEN
    v_new_status := 'accepted';
  ELSE
    v_new_status := 'pending';
  END IF;

  INSERT INTO lensers.relationships (source_profile_id, target_profile_id, status, accepted_at)
  VALUES (
    v_source_id,
    p_target_profile_id,
    v_new_status,
    CASE WHEN v_new_status = 'accepted' THEN now() ELSE null END
  )
  ON CONFLICT (source_profile_id, target_profile_id)
  DO UPDATE SET
    status       = v_new_status,
    requested_at = now(),
    responded_at = null,
    accepted_at  = CASE WHEN v_new_status = 'accepted' THEN now() ELSE null END,
    removed_at   = null;

  RETURN jsonb_build_object('status', v_new_status::text);
END;
$$;


ALTER FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."workspace_settings" (
    "ai_lenser_id" "uuid" NOT NULL,
    "default_model_id" "uuid",
    "default_provider_key" "text",
    "approval_default" "text" DEFAULT 'require_human'::"text" NOT NULL,
    "retention_days" integer DEFAULT 90 NOT NULL,
    "max_daily_credits" integer DEFAULT 1000 NOT NULL,
    "webhooks" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "api_access_enabled" boolean DEFAULT false NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "max_parallel_runs" integer DEFAULT 5 NOT NULL,
    "global_kill_switch" boolean DEFAULT false NOT NULL,
    "agent_paused" boolean DEFAULT false NOT NULL,
    "dark_launch_enabled" boolean DEFAULT false NOT NULL,
    "dark_launch_pct" integer DEFAULT 0 NOT NULL,
    "budget_enforce" boolean DEFAULT true NOT NULL,
    "runner_paused" boolean DEFAULT false NOT NULL,
    CONSTRAINT "workspace_settings_approval_default_check" CHECK (("approval_default" = ANY (ARRAY['auto'::"text", 'require_human'::"text", 'deny'::"text"]))),
    CONSTRAINT "workspace_settings_dark_launch_pct_check" CHECK ((("dark_launch_pct" >= 0) AND ("dark_launch_pct" <= 100)))
);


ALTER TABLE "agents"."workspace_settings" OWNER TO "postgres";


COMMENT ON TABLE "agents"."workspace_settings" IS 'Per-agent workspace settings: default model/provider, approval default, retention, daily budget, webhooks, API access. Owner-only via RLS.';



COMMENT ON COLUMN "agents"."workspace_settings"."agent_paused" IS 'DEPRECATED: use runner_paused. Kept in sync via trigger fn_workspace_settings_paused_sync. Will be removed in a future major migration; do not write directly.';



COMMENT ON COLUMN "agents"."workspace_settings"."runner_paused" IS 'Canonical pause flag for the workspace runner. Replaces agent_paused (deprecated).';



CREATE OR REPLACE FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS "agents"."workspace_settings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.workspace_settings;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id)
  VALUES (p_ai_lenser_id)
  ON CONFLICT (ai_lenser_id) DO NOTHING;

  UPDATE agents.workspace_settings
  SET metadata = metadata || jsonb_build_object(
        'deletion_requested_at', now(),
        'deletion_reason',       p_reason
      ),
      updated_at = now()
  WHERE ai_lenser_id = p_ai_lenser_id
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_requeue_battle_job_with_backoff(p_job_id, p_backoff_ms, p_error);
END;
$$;


ALTER FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text" DEFAULT NULL::"text", "p_model_override" "text" DEFAULT NULL::"text") RETURNS TABLE("model_id" "uuid", "model_key" "text", "provider_id" "uuid", "provider_key" "text")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
DECLARE
  v_provider_id uuid;
BEGIN
  -- Resolve optional provider override to a provider_id
  IF p_provider_override IS NOT NULL THEN
    SELECT p.id INTO v_provider_id
    FROM ai.providers p
    WHERE p.key = p_provider_override
    LIMIT 1;
  END IF;

  RETURN QUERY
  SELECT m.id, m.key, m.provider_id,
         (SELECT pr.key FROM ai.providers pr WHERE pr.id = m.provider_id LIMIT 1)
  FROM ai.models m
  WHERE m.is_active = true
    AND (v_provider_id IS NULL OR m.provider_id = v_provider_id)
    AND (
      p_model_override IS NULL
      OR m.id::text = p_model_override
      OR m.key      = p_model_override
    )
  ORDER BY m.created_at ASC
  LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") IS 'Security wrapper: resolve the best active AI model, optionally filtered by provider key and/or model key/id. Returns model_id, model_key, provider_id, provider_key.';



CREATE OR REPLACE FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $_$
DECLARE
  v_normalized text;
  v_user_id    uuid;
  v_email      text;
BEGIN
  -- 1. Normalise: strip leading @, trim whitespace, fold to lowercase
  v_normalized := lower(trim(regexp_replace(p_handle, '^@+', '')));

  -- 2. Reject inputs that cannot possibly be a stored handle
  --    (stored format: ^[a-z0-9._]{4,24}$)
  IF v_normalized !~ '^[a-z0-9._]{4,24}$' THEN
    RETURN NULL;
  END IF;

  -- 3. Look up the lenser profile — human accounts only, not deleted
  SELECT user_id
    INTO v_user_id
    FROM lensers.profiles
   WHERE handle = v_normalized
     AND type   = 'human'
     AND status NOT IN ('deleted'::lenser_status, 'pending_deletion'::lenser_status)
   LIMIT 1;

  IF v_user_id IS NULL THEN
    RETURN NULL;
  END IF;

  -- 4. Fetch the email from auth.users
  SELECT email
    INTO v_email
    FROM auth.users
   WHERE id         = v_user_id
     AND deleted_at IS NULL
   LIMIT 1;

  -- Returns NULL when auth record is gone (orphaned profile)
  RETURN v_email;
END;
$_$;


ALTER FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") IS 'Resolves a profile handle (username) to the associated auth.users email. Used exclusively by login flows to support handle+password authentication. Returns NULL for missing, deleted, or non-human handles. SECURITY: never surface the return value or its NULL state to end users; always return a generic "Invalid login credentials" error to callers.';



CREATE OR REPLACE FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) RETURNS TABLE("id" "uuid", "title" "text", "link" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
  SELECT
    l.id,
    COALESCE(t.title, '') AS title,
    '/lenses/' || l.id::text AS link
  FROM lenses.lenses l
  LEFT JOIN content.entity_translations t
    ON  t.entity_id   = l.id
    AND t.entity_type = 'lens'
    AND t.is_original = true
  WHERE l.id = ANY(p_ids)
    AND (
      l.visibility = 'public'
      OR l.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) IS 'Batch-resolves lens mention IDs to (id, title, link). Caller sees their own private lenses; other private lenses are excluded. Replaces N+1 getLensDetail calls in mentionService.';



CREATE OR REPLACE FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  UPDATE agents.run_incidents ri
  SET resolved_at = now(), resolution = p_resolution
  FROM agents.run_reports rr
  WHERE ri.id = p_incident_id
    AND ri.run_report_id = rr.id
    AND rr.ai_lenser_id IN (
      SELECT o.ai_lenser_id FROM agents.ownerships o
      WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
        AND o.revoked_at IS NULL
    );
$$;


ALTER FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
DECLARE
  v_actor UUID;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'Forbidden: super_admin required' USING ERRCODE = 'P0003';
  END IF;

  UPDATE audit.vote_anomalies
     SET resolved_at  = now(),
         resolved_by  = v_actor
   WHERE id = p_anomaly_id
     AND resolved_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Anomaly not found or already resolved' USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") IS 'CA: Marks an anomaly resolved. Requires super_admin.';



CREATE OR REPLACE FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('agent', p_ai_lenser_id); $$;


ALTER FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('lens', p_lens_id); $$;


ALTER FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('workflow', p_workflow_id); $$;


ALTER FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  PERFORM lenses.fn_restore_workflow_version(p_version_id);
END;
$$;


ALTER FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") IS 'Public wrapper: copies a historical version DAG back into the live workflow nodes/edges as a new draft without mutating the historical version. Owner-only. Delegates auth check to lenses.fn_restore_workflow_version.';



CREATE OR REPLACE FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, agent_paused)
  VALUES (p_ai_lenser_id, false)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET agent_paused = false,
        updated_at   = now();
END;
$$;


ALTER FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run     agents.team_runs;
  v_new_id  uuid;
BEGIN
  SELECT * INTO v_run
    FROM agents.team_runs
   WHERE id = p_run_id
   FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'team run not found' USING ERRCODE = 'P0002';
  END IF;

  IF v_run.ai_lenser_id <> p_ai_lenser_id THEN
    RAISE EXCEPTION 'run does not belong to this agent' USING ERRCODE = '42501';
  END IF;

  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF v_run.status NOT IN ('failed', 'cancelled', 'completed') THEN
    RAISE EXCEPTION 'only terminal runs can be retried' USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.team_runs (
    ai_lenser_id,
    team_id,
    workflow_id,
    workflow_assignment_id,
    status,
    approval_status,
    metadata
  ) VALUES (
    v_run.ai_lenser_id,
    v_run.team_id,
    v_run.workflow_id,
    v_run.workflow_assignment_id,
    'queued',
    'pending',
    jsonb_build_object('retried_from', v_run.id)
  )
  RETURNING id INTO v_new_id;

  INSERT INTO agents.agent_run_events (
    team_run_id,
    event_type,
    payload,
    occurred_at
  ) VALUES (
    v_new_id,
    'run_retried',
    jsonb_build_object(
      'original_run_id', v_run.id,
      'retried_by',      auth.uid()
    ),
    now()
  );

  RETURN v_new_id::text;
END;
$$;


ALTER FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") IS 'Clone a terminal (failed/cancelled/completed) team_run as a new queued run. Returns the new run id. Authorization via agents.can_manage_ai_lenser.';



CREATE OR REPLACE FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_retry_dead_letter_battle_job(p_dead_letter_id);
END;
$$;


ALTER FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_ownership agents.ownerships%ROWTYPE;
BEGIN
  SELECT *
  INTO v_ownership
  FROM agents.ownerships o
  WHERE o.id = p_ownership_id
  LIMIT 1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Ownership grant not found.'
      USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_ownership.ai_lenser_id) THEN
    RAISE EXCEPTION 'Not authorized to revoke this delegate.'
      USING ERRCODE = '42501';
  END IF;

  IF v_ownership.role = 'owner' THEN
    RAISE EXCEPTION 'The primary owner cannot be revoked through this RPC.'
      USING ERRCODE = '22023';
  END IF;

  UPDATE agents.ownerships
  SET revoked_at = now()
  WHERE id = p_ownership_id;
END;
$$;


ALTER FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") IS 'Revokes a co_owner or operator delegation for an AI Lenser. The primary owner must be handled through a dedicated ownership-transfer flow.';



CREATE OR REPLACE FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
BEGIN
    PERFORM ai.fn_revoke_api_key(p_key_id => p_key_id);
END;
$$;


ALTER FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") IS 'Public REST wrapper for ai.fn_revoke_api_key. Soft-revokes a BYOK API key — sets is_active=false, records revoked_at. Only the key owner can revoke (enforced inside ai.fn_revoke_api_key). The ai schema is not exposed in PostgREST — this wrapper is the only REST-callable entry point for key revocation. Added in migration 000057.';



CREATE OR REPLACE FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_caller       uuid := lensers.get_auth_lenser_id();
  v_ai_lenser_id uuid;
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  SELECT ai_lenser_id INTO v_ai_lenser_id
  FROM   agents.standing_approvals
  WHERE  id = p_id;

  IF v_ai_lenser_id IS NULL THEN
    RAISE EXCEPTION 'standing_approval_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_ai_lenser_id) THEN
    RAISE EXCEPTION 'not_owner_of_ai_lenser' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.standing_approvals
  SET    revoked_at = now()
  WHERE  id = p_id
    AND  revoked_at IS NULL;

  INSERT INTO agents.action_logs (
    ai_lenser_id, action_type, result, metadata
  ) VALUES (
    v_ai_lenser_id,
    'standing_approval_revoked',
    'success',
    jsonb_build_object(
      'standing_approval_id', p_id,
      'revoked_by',           v_caller
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") IS 'Phase Q2: revokes a standing approval (sets revoked_at=now()). Owner-only; writes an action_logs entry.';



CREATE OR REPLACE FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  DELETE FROM agents.tool_assignments
   WHERE ai_lenser_id = p_ai_lenser_id
     AND tool_id      = p_tool_id;
  RETURN true;
END;
$$;


ALTER FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_rls_unprotected_tables"() RETURNS TABLE("schema_name" "text", "table_name" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT n.nspname::text, c.relname::text
  FROM pg_catalog.pg_class c
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  WHERE c.relkind = 'r'
    AND n.nspname IN (
      'agents', 'lenses', 'battles', 'workflows',
      'audit', 'execution', 'devices', 'connectors'
    )
    AND NOT c.relrowsecurity
  ORDER BY n.nspname, c.relname;
$$;


ALTER FUNCTION "public"."fn_rls_unprotected_tables"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_rls_unprotected_tables"() IS 'BY: Returns tables in security-sensitive schemas that have relrowsecurity=false. Used by lf security rls-audit CLI command. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_eval  agents.evaluations;
  v_run   agents.evaluation_runs;
BEGIN
  SELECT * INTO v_eval FROM agents.evaluations WHERE id = p_evaluation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'evaluation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT (v_eval.owner_lenser_id = auth.uid()
          OR (v_eval.ai_lenser_id IS NOT NULL AND agents.can_manage_ai_lenser(v_eval.ai_lenser_id))) THEN
    RAISE EXCEPTION 'not authorized' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.evaluation_runs (evaluation_id, model_id, status)
  VALUES (p_evaluation_id, p_model_id, 'queued')
  RETURNING * INTO v_run;

  RETURN v_run.id;
END;
$$;


ALTER FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_id" "uuid" DEFAULT NULL::"uuid", "p_idempotency_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lenses', 'lensers'
    AS $$
DECLARE
  v_run_id uuid;
BEGIN
  SELECT execution.fn_run_lens_api(
    p_lens_id, p_version_id, p_model_id, p_inputs,
    p_funding_source, p_byok_key_id, p_idempotency_key
  ) INTO v_run_id;
  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") IS 'Security wrapper: queue a lens execution. Delegates to execution.fn_run_lens_api.';



CREATE OR REPLACE FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'devices', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_binding_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM execution.runners r
    WHERE r.id = p_runner_id AND r.lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM devices.registered_devices d
    WHERE d.id = p_device_id
      AND d.lenser_id = v_lenser_id
      AND d.trust_level IN ('approved','trusted')
  ) THEN
    RAISE EXCEPTION 'device_not_found_or_not_trusted' USING ERRCODE = '42501';
  END IF;

  INSERT INTO execution.runner_device_bindings (runner_id, device_id, status)
  VALUES (p_runner_id, p_device_id, 'active')
  ON CONFLICT (runner_id, device_id) DO UPDATE
    SET status = 'active', bound_at = now()
  RETURNING id INTO v_binding_id;

  RETURN v_binding_id;
END;
$$;


ALTER FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  UPDATE execution.runners
     SET is_active = true,
         updated_at = now()
   WHERE id = p_adapter_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") RETURNS TABLE("id" "uuid", "name" "text", "adapter_type" "text", "is_active" boolean, "config" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "last_probed_at" timestamp with time zone, "last_probe_status" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT r.id, r.name, r.adapter_type, r.is_active, r.config, r.created_at,
         r.updated_at, r.last_probed_at, r.last_probe_status
    FROM execution.runners r
   WHERE r.id = p_adapter_id
     AND r.lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_list"() RETURNS TABLE("id" "uuid", "name" "text", "adapter_type" "text", "is_active" boolean, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT r.id, r.name, r.adapter_type, r.is_active, r.created_at
    FROM execution.runners r
   WHERE r.lenser_id = v_lenser_id
   ORDER BY r.created_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_runner_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text" DEFAULT 'Hello, are you available?'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_runner    execution.runners%ROWTYPE;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_runner
    FROM execution.runners
   WHERE id = p_adapter_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT v_runner.is_active THEN
    RAISE EXCEPTION 'runner_inactive' USING ERRCODE = 'P0001';
  END IF;

  UPDATE execution.runners
     SET last_probed_at = now(),
         last_probe_status = 'queued',
         updated_at = now()
   WHERE id = p_adapter_id;

  RETURN jsonb_build_object(
    'runner_id',     v_runner.id,
    'adapter_type',  v_runner.adapter_type,
    'probe_status',  'queued',
    'response',      'Probe queued; daemon will execute when reachable.',
    'note',          'Network probes are performed by lf-gatewayd, not the database.'
  );
END;
$$;


ALTER FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_runner_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  IF p_adapter_type NOT IN ('openai-agents','langchain','crewai','mcp','ollama','http','custom') THEN
    RAISE EXCEPTION 'invalid_adapter_type: %', p_adapter_type USING ERRCODE = '22023';
  END IF;

  INSERT INTO execution.runners (lenser_id, name, adapter_type, config)
  VALUES (v_lenser_id, p_name, p_adapter_type, COALESCE(p_config, '{}'))
  RETURNING id INTO v_runner_id;

  RETURN v_runner_id;
END;
$$;


ALTER FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  -- Soft-deactivate (preserve audit trail).
  UPDATE execution.runners
     SET is_active = false,
         updated_at = now()
   WHERE id = p_adapter_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;

  -- Pause any active device bindings; do not delete (keeps history intact).
  UPDATE execution.runner_device_bindings
     SET status = 'paused'
   WHERE runner_id = p_adapter_id
     AND status = 'active';
END;
$$;


ALTER FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();

  -- Guard: caller must own the workflow that contains this node
  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_nodes n
    JOIN lenses.workflows w ON w.id = n.workflow_id
    WHERE n.id = p_node_id
      AND w.lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'node_not_found_or_forbidden';
  END IF;

  UPDATE lenses.workflow_nodes
  SET config = p_config
  WHERE id = p_node_id;
END;
$$;


ALTER FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_schedule_account_deletion"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_deadline timestamptz;
  v_profile RECORD;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT id, user_id, status
  INTO v_profile
  FROM lensers.profiles
  WHERE user_id = v_uid
  FOR UPDATE;

  IF NOT FOUND OR v_profile.status NOT IN ('active', 'deactivated') THEN
    RETURN jsonb_build_object('success', false, 'reason', 'not_eligible');
  END IF;

  v_deadline := now() + interval '30 days';

  -- Step 1: set the deletion timestamps. Existing trigger may temporarily move
  -- active profiles to `deactivated`, so we normalize status in step 2.
  UPDATE lensers.profiles
  SET deletion_requested_at = COALESCE(deletion_requested_at, now()),
      deletion_deadline_at = v_deadline,
      updated_at = now()
  WHERE id = v_profile.id;

  -- Step 2: persist the intended grace-period state used by purge + auth flow.
  UPDATE lensers.profiles
  SET status = 'pending_deletion',
      deletion_deadline_at = v_deadline,
      updated_at = now()
  WHERE id = v_profile.id;

  PERFORM lensers.log_account_lifecycle_event(
    v_profile.id,
    v_profile.user_id,
    'deletion_scheduled',
    v_profile.status,
    'pending_deletion'::lensers.lenser_status,
    'user_action',
    jsonb_build_object('deletion_deadline_at', v_deadline)
  );

  RETURN jsonb_build_object('success', true, 'status', 'pending_deletion', 'deadline', v_deadline);
END;
$$;


ALTER FUNCTION "public"."fn_schedule_account_deletion"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer DEFAULT 24, "p_auto_publish" boolean DEFAULT true) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  IF p_execution_starts_at IS NULL THEN
    RAISE EXCEPTION 'execution_starts_at must not be null.' USING ERRCODE = '22000';
  END IF;

  IF p_execution_starts_at <= now() THEN
    RAISE EXCEPTION 'execution_starts_at must be in the future (got %).',
      p_execution_starts_at USING ERRCODE = '22000';
  END IF;

  RETURN QUERY
  UPDATE battles.battles AS b
  SET
    execution_starts_at   = p_execution_starts_at,
    voting_duration_hours = COALESCE(p_voting_duration_hours, 24),
    auto_publish          = COALESCE(p_auto_publish, true)
  WHERE b.id = p_battle_id
    AND b.creator_lenser_id = v_lenser_id
  RETURNING
    b.id, b.slug, b.title, b.task_prompt, b.status::text, b.total_vote_count, b.published_at,
    b.voting_opens_at, b.voting_closes_at, b.battle_type::text, b.voter_eligibility::text, b.handicap_config,
    b.creator_lenser_id, b.forum_thread_id::text, b.workflow_id, b.lens_id,
    b.execution_starts_at, b.auto_publish, b.voting_duration_hours, b.vote_velocity, b.og_image_url,
    b.winner_contender_id, b.parent_battle_id, b.deleted_at;
END;
$$;


ALTER FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) IS 'Schedule a battle execution time. Rejects past timestamps server-side (issue #208). Casts enum and uuid-as-text columns to text to match declared return type.';



CREATE OR REPLACE FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer DEFAULT 8) RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "type" "lensers"."lenser_type")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  SELECT
    p.id,
    p.handle,
    p.display_name,
    p.avatar_url,
    p.type
  FROM lensers.profiles p
  WHERE
    p.status                = 'active'
    AND p.deletion_requested_at IS NULL
    AND (
      p.visibility::text = 'public'
      OR (p.visibility::text = 'community' AND lensers.get_auth_lenser_id() IS NOT NULL)
      OR p.id = lensers.get_auth_lenser_id()
    )
    AND (
      CASE
        WHEN left(p_query, 1) = '@' THEN
          p.handle ILIKE '%' || substring(p_query FROM 2) || '%'
        ELSE
          p.handle       ILIKE '%' || p_query || '%'
          OR p.display_name ILIKE '%' || p_query || '%'
      END
    )
  ORDER BY
    CASE
      WHEN left(p_query, 1) = '@' THEN
        CASE
          WHEN p.handle = lower(substring(p_query FROM 2))              THEN 0
          WHEN p.handle ILIKE lower(substring(p_query FROM 2)) || '%'   THEN 1
          ELSE 2
        END
      ELSE
        CASE
          WHEN p.handle = lower(p_query)     THEN 0
          WHEN p.handle ILIKE p_query || '%' THEN 1
          ELSE 2
        END
    END,
    p.handle
  LIMIT LEAST(COALESCE(p_limit, 8), 20);
$$;


ALTER FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) IS 'Search lensers by handle or display_name for battle invite / contender picker. Returns lenser type (human|ai) for wizard UI. Respects profile visibility: public=all, community=authenticated, private=self only. Prefix @ to restrict to handle-only search. LIMIT capped at 20. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "profile_id" "uuid", "ai_lenser_id" "uuid", "scope" "text", "source" "text", "content" "text", "confidence" numeric, "is_redacted" boolean, "created_at" timestamp with time zone, "rank" real)
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 20), 1), 50);
  v_query tsquery;
BEGIN
  IF p_query IS NULL OR length(trim(p_query)) = 0 THEN
    RETURN;
  END IF;

  v_query := plainto_tsquery('english', p_query);

  IF v_query::text = '' THEN
    -- The query string contained only stopwords; nothing to search for.
    RETURN;
  END IF;

  RETURN QUERY
    SELECT
      m.id,
      m.profile_id,
      m.ai_lenser_id,
      m.scope,
      m.source,
      m.content,
      m.confidence,
      m.is_redacted,
      m.created_at,
      ts_rank(to_tsvector('english', m.content), v_query) AS rank
    FROM agents.memories m
    WHERE
      (p_profile_id IS NULL OR m.profile_id = p_profile_id)
      AND m.is_redacted = false
      AND to_tsvector('english', m.content) @@ v_query
    ORDER BY rank DESC, m.created_at DESC
    LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) IS 'Phase N1: Full-text search over agents.memories.content using a plain english tsquery. Honors RLS via SECURITY INVOKER. Returns up to 50 rows ranked by ts_rank.';



CREATE OR REPLACE FUNCTION "public"."fn_security_definer_no_search_path"() RETURNS TABLE("schema_name" "text", "function_name" "text", "full_signature" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT
    n.nspname::text                          AS schema_name,
    p.proname::text                          AS function_name,
    pg_catalog.pg_get_function_identity_arguments(p.oid)::text AS full_signature
  FROM pg_catalog.pg_proc p
  JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
  WHERE p.prosecdef = true                          -- SECURITY DEFINER
    AND n.nspname IN ('public', 'agents', 'lenses',
                      'battles', 'audit', 'execution')
    AND NOT EXISTS (
      SELECT 1 FROM pg_catalog.pg_options_to_table(p.proconfig) AS opt(option_name, option_value)
      WHERE opt.option_name = 'search_path'
    )
  ORDER BY n.nspname, p.proname;
$$;


ALTER FUNCTION "public"."fn_security_definer_no_search_path"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_security_definer_no_search_path"() IS 'BY: Returns SECURITY DEFINER functions that have no SET search_path configured. Used by lf security rls-audit CLI command. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_score  numeric;
  v_result jsonb;
BEGIN
  SELECT score INTO v_score FROM agents.evaluation_runs WHERE id = p_run_id;

  INSERT INTO agents.evaluation_baselines (evaluation_id, run_id, score)
  VALUES (p_evaluation_id, p_run_id, v_score)
  ON CONFLICT (evaluation_id)
  DO UPDATE SET run_id = EXCLUDED.run_id, score = EXCLUDED.score
  RETURNING to_jsonb(agents.evaluation_baselines.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, runner_paused)
  VALUES (p_ai_lenser_id, p_paused)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET runner_paused = p_paused,
        updated_at    = now();

  -- Mirror legacy policy_evaluations row when pausing, matching fn_pause_agent shape.
  IF p_paused THEN
    INSERT INTO agents.policy_evaluations (
      ai_lenser_id,
      evaluation_point,
      policy_type,
      verdict,
      reason,
      context,
      evaluated_at
    ) VALUES (
      p_ai_lenser_id,
      'pre_run',
      'pause',
      'deny',
      'runner paused by owner',
      jsonb_build_object('paused_by', auth.uid(), 'source', 'fn_set_runner_paused'),
      now()
    );
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) IS 'Canonical pause/resume RPC. Prefer over fn_pause_agent / fn_resume_agent.';



CREATE OR REPLACE FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    calendar_id = p_calendar_id
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") IS 'Security wrapper: assign a calendar to a workflow schedule owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    pre_dispatch_condition = p_condition
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") IS 'Security wrapper: update pre_dispatch_condition on a workflow schedule.';



CREATE OR REPLACE FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    inputs_rotation = p_rotation
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") IS 'Security wrapper: replace inputs_rotation on a workflow schedule.';



CREATE OR REPLACE FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_catalog'
    AS $$
BEGIN
  IF p_secret IS NULL OR length(p_secret) < 16 THEN
    RAISE EXCEPTION 'webhook signing secret must be at least 16 chars'
      USING ERRCODE = '22023';
  END IF;
  -- Persist database-wide so subsequent sessions see it.
  EXECUTE format('ALTER DATABASE %I SET app.webhook_signing_secret = %L',
                 current_database(), p_secret);
END;
$$;


ALTER FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") IS 'Phase P. Persists app.webhook_signing_secret as a database-wide GUC. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_catalog'
    AS $$
BEGIN
  EXECUTE format('ALTER DATABASE %I SET app.webhook_strict_signing = %L',
                 current_database(),
                 CASE WHEN p_strict THEN 'true' ELSE 'false' END);
END;
$$;


ALTER FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  -- Self-notification check
  IF p_recipient_id = p_actor_id THEN
    RETURN false;
  END IF;

  -- Block check
  IF public.fn_is_notification_blocked(p_actor_id, p_recipient_id) THEN
    RETURN false;
  END IF;

  -- Mute check
  IF public.fn_is_notification_muted(p_recipient_id, p_type) THEN
    RETURN false;
  END IF;

  RETURN true;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_should_send_notification failed: %', SQLERRM;
  RETURN true;  -- fail-open — never suppress by accident
END;
$$;


ALTER FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_tournament    battles.tournaments;
  v_contenders    battles.tournament_contenders[];
  v_round         battles.tournament_rounds;
  i               INT;
  v_a             battles.tournament_contenders;
  v_b             battles.tournament_contenders;
  v_battle        battles.battles;
  v_match         battles.tournament_matches;
BEGIN
  SELECT * INTO v_tournament FROM battles.tournaments WHERE id = p_tournament_id;

  IF v_tournament.status <> 'registration' THEN
    RAISE EXCEPTION 'Tournament must be in registration status to start';
  END IF;

  -- Collect contenders (assign seeds by registration order)
  SELECT array_agg(tc ORDER BY tc.created_at)
  INTO   v_contenders
  FROM   battles.tournament_contenders tc
  WHERE  tc.tournament_id = p_tournament_id;

  IF array_length(v_contenders, 1) < 2 THEN
    RAISE EXCEPTION 'Need at least 2 contenders to start';
  END IF;

  -- Update seeds
  FOR i IN 1..array_length(v_contenders, 1) LOOP
    UPDATE battles.tournament_contenders
    SET seed = i, status = 'active'
    WHERE id = v_contenders[i].id;
  END LOOP;

  -- Create round 1
  INSERT INTO battles.tournament_rounds (tournament_id, round_number, status)
  VALUES (p_tournament_id, 1, 'active')
  RETURNING * INTO v_round;

  -- Pair contenders (1 vs N, 2 vs N-1, …) — standard bracket seeding
  i := 1;
  WHILE i <= array_length(v_contenders, 1) / 2 LOOP
    v_a := v_contenders[i];
    v_b := v_contenders[array_length(v_contenders, 1) - i + 1];

    -- Create an auto-starting battle for this match
    INSERT INTO battles.battles (
      title, task_prompt, battle_type, voter_eligibility, status, slug,
      execution_starts_at, auto_publish, voting_duration_hours,
      ai_judge_enabled, ai_judge_model_key,
      creator_lenser_id
    )
    SELECT
      v_tournament.title || ' — Round 1 Match ' || i,
      'Tournament match between ' || v_a.lenser_id || ' and ' || v_b.lenser_id,
      v_tournament.battle_type,
      'open',
      'open',
      lower(regexp_replace(v_tournament.title, '[^a-zA-Z0-9]', '-', 'g')) || '-r1m' || i,
      now() + INTERVAL '5 minutes',
      TRUE,
      24,
      v_tournament.ai_judge_enabled,
      CASE WHEN v_tournament.ai_judge_enabled THEN 'claude-sonnet-4-6' ELSE NULL END,
      v_tournament.creator_lenser_id
    RETURNING * INTO v_battle;

    -- Create match record
    INSERT INTO battles.tournament_matches
      (tournament_id, round_id, battle_id, contender_a_id, contender_b_id)
    VALUES
      (p_tournament_id, v_round.id, v_battle.id, v_a.id, v_b.id)
    RETURNING * INTO v_match;

    i := i + 1;
  END LOOP;

  -- Update tournament status
  UPDATE battles.tournaments
  SET status = 'active', updated_at = now()
  WHERE id = p_tournament_id;
END;
$$;


ALTER FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_global_model_id" "text" DEFAULT NULL::"text", "p_idempotency_key" "text" DEFAULT NULL::"text", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN lenses.fn_start_workflow_run(
    p_workflow_id,
    p_inputs,
    p_global_model_id,
    p_idempotency_key,
    p_version_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") IS 'Canonical workflow-start RPC (single overload). Accepts optional p_version_id to pin execution to a specific immutable version; NULL resolves to head_version_id. D2+D4+Z2: anon callers rejected; idempotency_key window capped at 24h.';



CREATE OR REPLACE FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text" DEFAULT NULL::"text", "p_raw_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
BEGIN
    RETURN ai.fn_store_api_key(
        p_provider => p_provider,
        p_label    => p_label,
        p_raw_key  => p_raw_key
    );
END;
$$;


ALTER FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") IS 'Public REST wrapper for ai.fn_store_api_key. Encrypts a BYOK API key via Vault and stores a reference row in ai.keys. The ai schema is not exposed in PostgREST — this wrapper is the only REST-callable entry point for key storage. Returns the key row UUID. Added in migration 000057.';



CREATE OR REPLACE FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "content_text" "text", "content_url" "text", "status" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  INSERT INTO battles.submissions (battle_id, contender_id, content_text, content_url, status)
  VALUES (p_battle_id, p_contender_id, p_content_text, NULL, 'submitted')
  RETURNING id, battle_id, contender_id, content_text, content_url, status;
$$;


ALTER FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean DEFAULT false, "p_rationale" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_vote_id   uuid;
BEGIN
  -- Guard: battle must be in voting phase
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles
    WHERE id = p_battle_id AND status = 'voting'
  ) THEN
    RAISE EXCEPTION 'battle_not_in_voting_phase: %', p_battle_id;
  END IF;

  -- Guard: contender must belong to the battle
  IF NOT EXISTS (
    SELECT 1 FROM battles.contenders
    WHERE id = p_voted_contender_id AND battle_id = p_battle_id
  ) THEN
    RAISE EXCEPTION 'contender_not_in_battle: %', p_voted_contender_id;
  END IF;

  -- Insert vote (uq_votes_battle_voter unique constraint prevents double-vote)
  INSERT INTO battles.votes (
    battle_id, voter_lenser_id, vote_value,
    voted_contender_id, is_draw, rationale
  )
  VALUES (
    p_battle_id, v_lenser_id, p_vote_value,
    p_voted_contender_id, p_is_draw, p_rationale
  )
  RETURNING id INTO v_vote_id;

  -- Upsert vote aggregates (atomic counter update)
  INSERT INTO battles.vote_aggregates (
    battle_id, contender_id,
    raw_vote_count, weighted_vote_sum, draw_count
  )
  VALUES (
    p_battle_id, p_voted_contender_id,
    1, 1, CASE WHEN p_is_draw THEN 1 ELSE 0 END
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE SET
    raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
    weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
    draw_count        = battles.vote_aggregates.draw_count +
                        CASE WHEN p_is_draw THEN 1 ELSE 0 END,
    updated_at        = now();

  -- Increment denormalized total on the battle row
  UPDATE battles.battles
  SET total_vote_count = total_vote_count + 1
  WHERE id = p_battle_id;

  -- TODO Phase C: award XP via xp rule lookup once rule_id for 'BATTLE_VOTED' is known.
  -- INSERT INTO xp.events (lenser_id, app_id, rule_id, action_key, xp, base_xp, source,
  --                        source_ref_type, source_ref_id)
  -- VALUES (v_lenser_id, <app_id>, <rule_id>, 'BATTLE_VOTED', <xp>, <base_xp>, 'votes',
  --         'battle', p_battle_id);

  RETURN jsonb_build_object(
    'vote_id',   v_vote_id,
    'status',    'success',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") IS 'Canonical vote submission path. Atomically: 1) inserts into battles.votes (uq_votes_battle_voter prevents double-vote), 2) upserts battles.vote_aggregates, 3) increments battles.total_vote_count. Phase A fix: removed broken xp.events INSERT (wrong columns). XP award deferred to Phase C once xp rule_id for BATTLE_VOTED is known. Direct INSERT on battles.votes is restricted to service_role (authenticated policy dropped in A2).';



CREATE OR REPLACE FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean DEFAULT false, "p_rationale" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_voter_lenser_id  uuid;
  v_vote_id   uuid;
BEGIN
  v_voter_lenser_id := lensers.get_auth_lenser_id();

  -- Rate-limit: one vote per voter per battle per 60 s
  IF NOT battles.fn_check_vote_rate_limit(v_voter_lenser_id, p_battle_id) THEN
    RAISE EXCEPTION 'RATE_LIMIT: vote rate limit exceeded for battle %', p_battle_id
      USING ERRCODE = 'P0429';
  END IF;

  INSERT INTO battles.votes (
    battle_id,
    voter_lenser_id,
    voted_contender_id,
    vote_value,
    is_draw,
    rationale
  )
  VALUES (
    p_battle_id,
    v_voter_lenser_id,
    p_voted_contender_id,
    p_vote_value,
    p_is_draw,
    p_rationale
  )
  RETURNING id INTO v_vote_id;

  RETURN jsonb_build_object(
    'vote_id',   v_vote_id,
    'status',    'submitted',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_profile     agents.memory_profiles;
  v_count       integer;
  v_last_at     timestamptz;
  v_scope_stats jsonb;
BEGIN
  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  SELECT COUNT(*), MAX(created_at)
    INTO v_count, v_last_at
    FROM agents.memories
   WHERE profile_id = p_profile_id AND is_redacted = false;

  SELECT COALESCE(jsonb_object_agg(scope, c), '{}'::jsonb)
    INTO v_scope_stats
    FROM (
      SELECT scope, COUNT(*) AS c
        FROM agents.memories
       WHERE profile_id = p_profile_id AND is_redacted = false
       GROUP BY scope
    ) s;

  RETURN jsonb_build_object(
    'profile_id',      p_profile_id,
    'count',           COALESCE(v_count, 0),
    'last_written_at', v_last_at,
    'scopes',          v_scope_stats
  );
END;
$$;


ALTER FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") IS 'Stub summary returning counts and last write. Replaceable by an LLM/embedding-backed summarizer behind the same signature (Open-Closed).';



CREATE OR REPLACE FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
DECLARE
  v_human_id          uuid;
  v_current_active_id uuid;
  v_from_ai_lenser_id uuid;
  v_target_profile_id uuid;
  v_to_ai_lenser_id   uuid;
BEGIN
  SELECT p.id INTO v_human_id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.type    = 'human'
    AND p.status  = 'active'
  LIMIT 1;

  IF v_human_id IS NULL THEN
    RAISE EXCEPTION 'No active human profile found for this user';
  END IF;

  SELECT pref.active_lenser_id INTO v_current_active_id
  FROM lensers.preferences pref
  WHERE pref.lenser_id = v_human_id
  LIMIT 1;

  SELECT al.id INTO v_from_ai_lenser_id
  FROM agents.ai_lensers al
  JOIN lensers.profiles ap ON ap.id = al.profile_id
  WHERE ap.id = v_current_active_id
  LIMIT 1;

  IF p_lenser_id = v_human_id THEN
    UPDATE lensers.preferences
    SET active_lenser_id = NULL,
        updated_at       = now()
    WHERE lenser_id = v_human_id;

    IF v_from_ai_lenser_id IS NOT NULL THEN
      INSERT INTO agents.workspace_switches (human_lenser_id, from_ai_lenser_id, to_ai_lenser_id)
      VALUES (v_human_id, v_from_ai_lenser_id, NULL);
    END IF;

    RETURN;
  END IF;

  SELECT ai_p.id, al.id
  INTO v_target_profile_id, v_to_ai_lenser_id
  FROM agents.ownerships o
  JOIN agents.ai_lensers al ON al.id = o.ai_lenser_id
  JOIN lensers.profiles ai_p ON ai_p.id = al.profile_id
  WHERE o.owner_lenser_id = v_human_id
    AND (ai_p.id = p_lenser_id OR al.id = p_lenser_id)
    AND o.role = 'owner'
    AND o.revoked_at IS NULL
    AND ai_p.status = 'active'
  LIMIT 1;

  IF v_target_profile_id IS NULL THEN
    RAISE EXCEPTION 'Cannot switch: profile not found or not owned'
      USING ERRCODE = '42501';
  END IF;

  UPDATE lensers.preferences
  SET active_lenser_id = v_target_profile_id,
      updated_at       = now()
  WHERE lenser_id = v_human_id;

  INSERT INTO agents.workspace_switches (human_lenser_id, from_ai_lenser_id, to_ai_lenser_id)
  VALUES (v_human_id, v_from_ai_lenser_id, v_to_ai_lenser_id);
END;
$$;


ALTER FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") IS 'Switches the authenticated user''s active workspace to the given lenser ID. Accepts the human profile id, an owned AI profile id, or that owned AI lenser runtime id. Stores AI selection in lensers.preferences.active_lenser_id as the profile id and appends agents.workspace_switches audit rows.';



CREATE OR REPLACE FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "object_class" "text", "object_id" "text", "op" "text", "payload" "jsonb", "vclock" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_sync_pull(p_envelope, p_object_classes, p_limit);
$$;


ALTER FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") RETURNS TABLE("applied_count" integer, "rejected_count" integer, "rejections" "jsonb")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_sync_push(p_envelope);
$$;


ALTER FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") RETURNS TABLE("object_class" "text", "watermark" timestamp with time zone, "outbox_depth" bigint, "last_error" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_sync_status(p_device_id);
$$;


ALTER FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$
BEGIN
  INSERT INTO analytics.tag_activity_events (tag_id, entity_type, entity_id, activity_type, actor_id)
  SELECT 
    (e->>'tag_id')::uuid,
    ((e->>'entity_type')::content.entity_type_enum),
    (e->>'entity_id')::uuid,
    (e->>'activity_type')::text,
    (e->>'actor_id')::uuid
  FROM jsonb_array_elements(p_events) AS e
  WHERE (e->>'tag_id') IS NOT NULL 
    AND (e->>'entity_id') IS NOT NULL;
END;
$$;


ALTER FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text" DEFAULT 'info'::"text", "p_node_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_caller_id uuid := lensers.get_auth_lenser_id();
  v_tag_id    uuid;
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF p_severity NOT IN ('info', 'warn', 'critical') THEN
    RAISE EXCEPTION 'invalid severity: %', p_severity USING ERRCODE = 'check_violation';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    WHERE r.id = p_run_id
      AND r.triggered_by = v_caller_id
  ) THEN
    RAISE EXCEPTION 'workflow_run % not found or not owned by caller', p_run_id
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  INSERT INTO lenses.workflow_run_tags (run_id, tag, severity, node_id, metadata)
  VALUES (p_run_id, p_tag, p_severity, p_node_id, p_metadata)
  RETURNING id INTO v_tag_id;

  RETURN v_tag_id;
END;
$$;


ALTER FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") IS 'Classifies a workflow run with a tag (info|warn|critical). Owner-gated. Called by the browser executor + CF Worker to mark moderation_blocked / contract_violated / idempotent_replay / partial_success / high_token_usage runs. See docs/reference/workflows/execution-engine.md §Observability.';



CREATE OR REPLACE FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer DEFAULT 50) RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "visibility" "text", "created_at" timestamp with time zone, "created_count" bigint, "viewed_count" bigint, "reacted_count" bigint, "total_usage" bigint, "trend_score_7d" numeric)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$
  SELECT
    id,
    slug,
    name,
    visibility,
    created_at,
    created_count,
    viewed_count,
    reacted_count,
    total_usage,
    trend_score_7d
  FROM vw_tags_public_stats
  ORDER BY trend_score_7d DESC NULLS LAST, total_usage DESC NULLS LAST
  LIMIT LEAST(p_limit, 100);
$$;


ALTER FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text" DEFAULT 'en'::"text", "p_limit" integer DEFAULT 5) RETURNS TABLE("id" "uuid", "name" "text", "slug" "text", "visibility" "text", "total_usage" bigint, "lang_match" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
  SELECT
    t.id,
    COALESCE(tr_lang.name, tr_en.name, t.slug) AS name,
    t.slug,
    t.visibility::text,
    COALESCE(s.total_usage, 0)                  AS total_usage,
    (tr_lang.tag_id IS NOT NULL)                AS lang_match
  FROM content.tags t
  LEFT JOIN content.tag_translations tr_lang
    ON tr_lang.tag_id = t.id AND tr_lang.language_code = p_lang
  LEFT JOIN content.tag_translations tr_en
    ON tr_en.tag_id = t.id AND tr_en.language_code = 'en'
  LEFT JOIN vw_tags_public_stats s
    ON s.id = t.id
  WHERE
    t.visibility = 'public'
    AND (
      COALESCE(tr_lang.name, tr_en.name, t.slug) ILIKE '%' || p_query || '%'
      OR t.slug ILIKE '%' || p_query || '%'
    )
  ORDER BY
    lang_match DESC,
    CASE
      WHEN LOWER(COALESCE(tr_lang.name, tr_en.name, t.slug)) = LOWER(p_query) THEN 0
      ELSE 1
    END,
    COALESCE(s.total_usage, 0) DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_auto_start_delay_hours" integer DEFAULT 1) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  UPDATE battles.templates
  SET recurrence_rule = p_recurrence_rule,
      next_run_at     = COALESCE(p_next_run_at, now())
  WHERE id = p_template_id
    AND creator_lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer DEFAULT 4) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_threshold INTERVAL;
  v_count     INT := 0;
  v_run_id    UUID;
BEGIN
  v_threshold := make_interval(hours => p_stale_threshold_hours);

  FOR v_run_id IN
    SELECT id
    FROM   lenses.workflow_runs
    WHERE  status IN ('running', 'streaming', 'recovered')
      AND  (
             -- No heartbeat and started too long ago
             (heartbeat_at IS NULL     AND started_at  < now() - v_threshold)
             OR
             -- Heartbeat present but stale
             (heartbeat_at IS NOT NULL AND heartbeat_at < now() - v_threshold)
           )
    FOR UPDATE SKIP LOCKED
  LOOP
    -- Mark run terminal
    UPDATE lenses.workflow_runs
    SET    status       = 'timed_out',
           completed_at = COALESCE(completed_at, now())
    WHERE  id = v_run_id;

    -- Cascade: mark all non-terminal node results as timed_out
    UPDATE lenses.workflow_node_results
    SET    status        = 'timed_out',
           completed_at  = COALESCE(completed_at, now()),
           error_message = COALESCE(error_message, 'run_timed_out_by_sweeper')
    WHERE  run_id = v_run_id
      AND  status NOT IN (
             'completed', 'failed', 'cancelled',
             'timed_out', 'skipped', 'blocked', 'invalidated'
           );

    v_count := v_count + 1;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) IS 'Sweeper: marks workflow runs that have been in running/streaming/recovered state with no heartbeat for > p_stale_threshold_hours (default 4h) as timed_out, cascading to node results. Uses FOR UPDATE SKIP LOCKED to avoid contention with live workers. Intended to run every 5 minutes via pg_cron.';



CREATE OR REPLACE FUNCTION "public"."fn_timeout_stale_runs_safe"() RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT automation.fn_run_with_lock(
    'timeout-stale-runs',
    'SELECT public.fn_timeout_stale_runs(4)'
  );
$$;


ALTER FUNCTION "public"."fn_timeout_stale_runs_safe"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_timeout_stale_runs_safe"() IS 'pg_cron entry-point for fn_timeout_stale_runs. Wraps execution in automation.fn_run_with_lock (advisory lock + 55s statement_timeout) to prevent overlapping ticks.';



CREATE OR REPLACE FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "name" "text", "match_event_type" "text", "match_filter" "jsonb", "action_kind" "text", "action_config" "jsonb", "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  UPDATE automation.trigger_rules
  SET    is_active  = p_is_active,
         updated_at = now()
  WHERE  id        = p_rule_id
    AND  lenser_id = auth.uid()
  RETURNING
    id, lenser_id, name, match_event_type, match_filter,
    action_kind, action_config, is_active, created_at, updated_at;
$$;


ALTER FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) IS 'Security wrapper: set is_active on a trigger rule owned by the current user. Returns the updated row. No-ops silently if the rule does not belong to the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  UPDATE battles.templates
  SET is_public  = p_is_public,
      updated_at = now()
  WHERE id = p_template_id;
$$;


ALTER FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, global_kill_switch)
  VALUES (p_ai_lenser_id, p_enabled)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET global_kill_switch = p_enabled,
        updated_at         = now();

  IF p_enabled THEN
    INSERT INTO agents.policy_evaluations (
      ai_lenser_id,
      evaluation_point,
      policy_type,
      verdict,
      reason,
      context,
      evaluated_at
    ) VALUES (
      p_ai_lenser_id,
      'pre_run',
      'kill_switch',
      'deny',
      'global kill switch activated',
      jsonb_build_object('activated_by', auth.uid()),
      now()
    );
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
DECLARE
  v_owner_id UUID;
BEGIN
  IF p_visibility NOT IN ('public', 'private', 'unlisted') THEN
    RAISE EXCEPTION 'Invalid visibility value: %. Must be public, private, or unlisted', p_visibility
      USING ERRCODE = 'P0001';
  END IF;

  SELECT owner_lenser_id INTO v_owner_id
  FROM media.objects
  WHERE id = p_object_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'media_object_not_found: %', p_object_id USING ERRCODE = 'P0001';
  END IF;

  IF v_owner_id != auth.uid() THEN
    RAISE EXCEPTION 'media_visibility_forbidden: caller does not own object %', p_object_id
      USING ERRCODE = 'P0001';
  END IF;

  UPDATE media.objects
  SET visibility = p_visibility
  WHERE id = p_object_id;

  -- Audit row written by trg_media_audit_visibility trigger (see below)
END;
$$;


ALTER FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") IS 'AT: Toggles visibility of a media object. Owner only. The audit trigger writes audit.events. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    is_active  = p_is_active
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) IS 'Security wrapper: toggle is_active on a workflow schedule owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'public'
    AS $$
BEGIN
  UPDATE media.objects
  SET owner_lenser_id = p_new_owner_id
  WHERE id = p_object_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'media_object_not_found: %', p_object_id USING ERRCODE = 'P0001';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") IS 'AT: Transfers media object ownership. Service_role only (used on agent deletion). SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_trg_battles_status_webhook"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  IF NEW.status IS DISTINCT FROM OLD.status THEN
    PERFORM public.fn_battles_notify_webhooks(
      NEW.id,
      'status_change',
      row_to_json(NEW)::JSONB
    );
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."fn_trg_battles_status_webhook"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_trg_votes_velocity_check"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  -- Skip rate-limit check for service_role and postgres (seed/admin inserts)
  IF auth.role() = 'authenticated' THEN
    PERFORM public.fn_check_vote_velocity(NEW.voter_lenser_id);
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."fn_trg_votes_velocity_check"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id uuid;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  DELETE FROM lensers.relationships
  WHERE source_profile_id = v_source_id
    AND target_profile_id = p_target_profile_id
    AND status = 'blocked';

  RETURN jsonb_build_object('blocked', false);
END;
$$;


ALTER FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_caller_human_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = v_caller_human_id
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  UPDATE agents.ai_lensers
  SET personality_note = p_personality_note
  WHERE id = p_ai_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") IS 'Owner-only update of an AI lenser personality_note. Authorizes via the human lenser ownership record.';



CREATE OR REPLACE FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  PERFORM agents.fn_update_agent_policy(p_ai_lenser_id, p_patch);
END;
$$;


ALTER FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
DECLARE
  v_profile_id uuid;
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM   agents.ownerships o
    WHERE  o.ai_lenser_id   = p_ai_lenser_id
      AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND  o.role            = 'owner'
      AND  o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  SELECT al.profile_id INTO v_profile_id
  FROM   agents.ai_lensers al
  WHERE  al.id = p_ai_lenser_id;

  IF v_profile_id IS NULL THEN
    RAISE EXCEPTION 'AI lenser not found'
      USING ERRCODE = 'P0002';
  END IF;

  UPDATE lensers.profiles
  SET
    display_name = CASE WHEN p_patch ? 'display_name' THEN p_patch->>'display_name' ELSE display_name END,
    avatar_url   = CASE WHEN p_patch ? 'avatar_url'   THEN p_patch->>'avatar_url'   ELSE avatar_url   END,
    banner_url   = CASE WHEN p_patch ? 'banner_url'   THEN p_patch->>'banner_url'   ELSE banner_url   END,
    bio          = CASE WHEN p_patch ? 'bio'          THEN p_patch->>'bio'          ELSE bio          END,
    headline     = CASE WHEN p_patch ? 'headline'     THEN p_patch->>'headline'     ELSE headline     END,
    website_url  = CASE WHEN p_patch ? 'website_url'  THEN p_patch->>'website_url'  ELSE website_url  END,
    updated_at   = now()
  WHERE id   = v_profile_id
    AND type = 'ai';
END;
$$;


ALTER FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") IS 'Allows an owner lenser to patch display_name, avatar_url, banner_url, bio, headline, website_url of an AI lenser they own. Validates ownership via agents.ownerships.';



CREATE OR REPLACE FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text" DEFAULT NULL::"text", "p_task_prompt" "text" DEFAULT NULL::"text", "p_battle_type" "text" DEFAULT NULL::"text", "p_voter_eligibility" "text" DEFAULT NULL::"text", "p_handicap_config" "jsonb" DEFAULT NULL::"jsonb", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_lens_id" "uuid" DEFAULT NULL::"uuid", "p_forum_thread_id" "uuid" DEFAULT NULL::"uuid", "p_shared_input_snapshot" "jsonb" DEFAULT NULL::"jsonb", "p_lenser_battle_policy" "jsonb" DEFAULT NULL::"jsonb", "p_task_source" "text" DEFAULT NULL::"text", "p_contender_structure" "text" DEFAULT NULL::"text", "p_judging_mode" "text" DEFAULT NULL::"text", "p_challenge_type" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone, "task_source" "text", "contender_structure" "text", "judging_mode" "text", "challenge_type" "text", "shared_input_snapshot" "jsonb", "lenser_battle_policy" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  UPDATE battles.battles b
  SET
    title                 = COALESCE(p_title,                                              b.title),
    task_prompt           = COALESCE(p_task_prompt,                                        b.task_prompt),
    battle_type           = COALESCE(p_battle_type::battles.battle_type_enum,              b.battle_type),
    voter_eligibility     = COALESCE(p_voter_eligibility::battles.voter_eligibility_enum,  b.voter_eligibility),
    handicap_config       = COALESCE(p_handicap_config,                                    b.handicap_config),
    workflow_id           = COALESCE(p_workflow_id,                                        b.workflow_id),
    lens_id               = COALESCE(p_lens_id,                                            b.lens_id),
    forum_thread_id       = COALESCE(p_forum_thread_id,                                    b.forum_thread_id),
    shared_input_snapshot = COALESCE(p_shared_input_snapshot,                              b.shared_input_snapshot),
    lenser_battle_policy  = COALESCE(p_lenser_battle_policy,                               b.lenser_battle_policy),
    task_source           = COALESCE(p_task_source,                                        b.task_source),
    contender_structure   = COALESCE(p_contender_structure::battles.contender_structure_enum, b.contender_structure),
    judging_mode          = COALESCE(p_judging_mode::battles.judging_mode_enum,            b.judging_mode),
    challenge_type        = COALESCE(p_challenge_type,                                     b.challenge_type)
  WHERE b.id = p_battle_id
    AND b.creator_lenser_id = v_lenser_id
  RETURNING
    b.id, b.slug, b.title, b.task_prompt, b.status::text, b.total_vote_count,
    b.published_at, b.voting_opens_at, b.voting_closes_at,
    b.battle_type::text, b.voter_eligibility::text, b.handicap_config,
    b.creator_lenser_id, b.forum_thread_id::text, b.workflow_id, b.lens_id,
    b.execution_starts_at, b.auto_publish, b.voting_duration_hours,
    b.vote_velocity, b.og_image_url, b.winner_contender_id,
    b.parent_battle_id, b.deleted_at,
    b.task_source, b.contender_structure::text, b.judging_mode::text,
    b.challenge_type, b.shared_input_snapshot, b.lenser_battle_policy;
END;
$$;


ALTER FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") IS 'Update a battle draft. Caller must be the creator. All params are optional — only non-null values overwrite. Now accepts and returns V2 concept-separation axes: task_source, contender_structure, judging_mode, challenge_type.';



CREATE OR REPLACE FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  IF p_execution_starts_at IS NULL THEN
    RAISE EXCEPTION 'execution_starts_at must not be null.' USING ERRCODE = '22000';
  END IF;

  IF p_execution_starts_at <= now() THEN
    RAISE EXCEPTION 'execution_starts_at must be in the future (got %).',
      p_execution_starts_at USING ERRCODE = '22000';
  END IF;

  UPDATE battles.battles
  SET    execution_starts_at   = p_execution_starts_at,
         voting_duration_hours = p_voting_duration_hours,
         auto_publish          = p_auto_publish
  WHERE  id = p_battle_id
    AND  creator_lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) IS 'Security wrapper: update execution scheduling fields on a battle owned by the current user. Rejects past execution timestamps server-side (issue #208).';



CREATE OR REPLACE FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_auth_lenser_id uuid;
  v_owner_lenser_id uuid;
BEGIN
  v_auth_lenser_id := lensers.get_auth_lenser_id();

  -- Resolve the team owner
  SELECT al.id INTO v_owner_lenser_id
  FROM agents.teams t
  JOIN agents.ai_lensers al ON al.id = t.ai_lenser_id
  WHERE t.id = p_team_id;

  IF v_owner_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Team % not found', p_team_id
      USING ERRCODE = 'P0002';
  END IF;

  -- Only the ai_lenser whose profile_id is the auth user may update roles
  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    WHERE al.id = v_owner_lenser_id
      AND al.profile_id = v_auth_lenser_id
  ) THEN
    RAISE EXCEPTION 'Caller is not the owner of team %', p_team_id
      USING ERRCODE = '42501';
  END IF;

  -- Validate the target member belongs to the team
  IF NOT EXISTS (
    SELECT 1 FROM agents.team_members
    WHERE id = p_member_id AND team_id = p_team_id
  ) THEN
    RAISE EXCEPTION 'Member % not found in team %', p_member_id, p_team_id
      USING ERRCODE = 'P0002';
  END IF;

  UPDATE agents.team_members
  SET role = p_role, updated_at = now()
  WHERE id = p_member_id AND team_id = p_team_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") IS 'Updates a team member role. Caller must own the team. Only the role field is mutated — other member fields require direct RLS access.';



CREATE OR REPLACE FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT public.fn_worker_update_team_run(p_team_run_id, p_status, p_completed_at);
$$;


ALTER FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM content.threads
    WHERE id = p_thread_id AND lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RETURN;
  END IF;

  UPDATE content.entity_translations
  SET    title      = p_title,
         content    = p_content,
         updated_at = now()
  WHERE  entity_type::text = 'thread'
    AND  entity_id   = p_thread_id
    AND  is_original = true;
END;
$$;


ALTER FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") IS 'Security wrapper: update title and content of the original translation for a thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  UPDATE content.threads
  SET    visibility = p_visibility::content.visibility_enum,
         updated_at = now()
  WHERE  id        = p_thread_id
    AND  lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") IS 'Security wrapper: update visibility on a thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_visibility" "text" DEFAULT 'public'::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
declare
  v_lenser_id uuid;
begin
  if auth.uid() is null then
    raise exception 'Not authenticated';
  end if;

  v_lenser_id := lensers.get_auth_lenser_id();

  if v_lenser_id is null then
    raise exception 'Profile not found';
  end if;

  if char_length(trim(p_title)) < 1 or char_length(trim(p_title)) > 200 then
    raise exception 'Title must be between 1 and 200 characters';
  end if;

  if p_visibility not in ('public', 'private', 'unlisted') then
    raise exception 'Invalid visibility value';
  end if;

  return query
  update lenses.workflows w
  set
    title       = trim(p_title),
    description = p_description,
    visibility  = p_visibility,
    updated_at  = now()
  where w.id        = p_workflow_id
    and w.lenser_id = v_lenser_id
  returning
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility,
    w.battle_count,
    w.created_at,
    w.updated_at;

  if not found then
    raise exception 'Workflow not found or not owned by caller';
  end if;
end;
$$;


ALTER FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") IS 'Update workflow title, description, and visibility. Ownership resolved via lensers.get_auth_lenser_id() (supports AI lenser context switching). SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text", "p_retry_count" integer DEFAULT NULL::integer, "p_duration_ms" integer DEFAULT NULL::integer, "p_ttfb_ms" integer DEFAULT NULL::integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot update results for run %', p_run_id
      USING ERRCODE = '42501';
  END IF;

  UPDATE lenses.workflow_node_results
  SET
    status        = p_status,
    output_data   = COALESCE(p_output_data, output_data),
    error_message = COALESCE(p_error_message, error_message),
    retry_count   = COALESCE(p_retry_count, retry_count),
    -- Timing metrics are monotonic: once written, later partial updates
    -- (e.g. failed -> retrying -> completed) keep the most recent non-null
    -- value for duration, but ttfb only records the FIRST streamed chunk.
    duration_ms   = COALESCE(p_duration_ms, duration_ms),
    ttfb_ms       = COALESCE(ttfb_ms, p_ttfb_ms),
    started_at    = CASE
                      WHEN p_status IN ('running', 'streaming', 'retrying') AND started_at IS NULL
                        THEN now()
                      ELSE started_at
                    END,
    completed_at  = CASE
                      WHEN p_status IN ('completed', 'failed', 'cancelled',
                                        'skipped', 'timed_out', 'blocked', 'invalidated')
                        THEN now()
                      ELSE completed_at
                    END
  WHERE run_id = p_run_id AND node_id = p_node_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) IS 'Phase 6 overload: persists retry_count / duration_ms / ttfb_ms alongside status + output. All observability args are nullable — callers that omit them preserve prior values.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text", "p_retry_count" integer DEFAULT NULL::integer, "p_duration_ms" integer DEFAULT NULL::integer, "p_ttfb_ms" integer DEFAULT NULL::integer, "p_waiting_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot update results for run %', p_run_id
      USING ERRCODE = '42501';
  END IF;

  UPDATE lenses.workflow_node_results
  SET
    status        = p_status,
    output_data   = COALESCE(p_output_data, output_data),
    error_message = COALESCE(p_error_message, error_message),
    retry_count   = COALESCE(p_retry_count, retry_count),
    duration_ms   = COALESCE(p_duration_ms, duration_ms),
    ttfb_ms       = COALESCE(ttfb_ms, p_ttfb_ms),
    -- Waiting reason: explicit overrides existing; null clears once the node
    -- transitions out of a waiting status.
    waiting_reason = CASE
                       WHEN p_status IN ('awaiting_dependency', 'queued', 'retrying')
                         THEN COALESCE(p_waiting_reason, waiting_reason)
                       ELSE NULL
                     END,
    started_at    = CASE
                      WHEN p_status IN ('running', 'streaming', 'retrying') AND started_at IS NULL
                        THEN now()
                      ELSE started_at
                    END,
    completed_at  = CASE
                      WHEN p_status IN ('completed', 'failed', 'cancelled',
                                        'skipped', 'timed_out', 'blocked', 'invalidated')
                        THEN now()
                      ELSE completed_at
                    END
  WHERE run_id = p_run_id AND node_id = p_node_id;

  -- Maintain run-level active node pointer (n8n-style "currently executing").
  -- Active when entering running/streaming/retrying, cleared on terminal.
  IF p_status IN ('running', 'streaming', 'retrying') THEN
    UPDATE lenses.workflow_runs
    SET active_node_id = p_node_id
    WHERE id = p_run_id;
  ELSIF p_status IN ('completed', 'failed', 'cancelled',
                     'skipped', 'timed_out', 'blocked', 'invalidated') THEN
    UPDATE lenses.workflow_runs
    SET active_node_id = NULL
    WHERE id = p_run_id
      AND active_node_id = p_node_id;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") IS 'Canonical n8n-style node result update. Persists status + output + observability metrics, maintains waiting_reason for non-running statuses, and updates workflow_runs.active_node_id so the inspector can render the active step without scanning all node rows.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot update run %', p_run_id
      USING ERRCODE = '42501';
  END IF;

  UPDATE lenses.workflow_runs
  SET
    status       = p_status,
    started_at   = CASE
                     WHEN p_status IN ('running', 'streaming') AND started_at IS NULL
                       THEN now()
                     ELSE started_at
                   END,
    completed_at = CASE
                     WHEN p_status IN ('completed', 'failed', 'cancelled', 'timed_out')
                       THEN now()
                     ELSE completed_at
                   END,
    -- Clear worker ownership on terminal transitions so stale-claim queries
    -- do not revive a finished run.
    run_worker_id = CASE
                      WHEN p_status IN ('completed', 'failed', 'cancelled', 'timed_out')
                        THEN NULL
                      ELSE run_worker_id
                    END
  WHERE id = p_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") IS 'Updates a workflow_run status. Aligned with Phase 1 state machine — started_at written on first transition into running/streaming, completed_at for terminal states (completed, failed, cancelled, timed_out), run_worker_id cleared on terminals.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") RETURNS "agents"."workspace_settings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.workspace_settings;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id)
  VALUES (p_ai_lenser_id)
  ON CONFLICT (ai_lenser_id) DO NOTHING;

  UPDATE agents.workspace_settings
  SET default_model_id     = COALESCE((p_patch->>'default_model_id')::uuid,     default_model_id),
      default_provider_key = COALESCE(p_patch->>'default_provider_key',         default_provider_key),
      approval_default     = COALESCE(p_patch->>'approval_default',             approval_default),
      retention_days       = COALESCE((p_patch->>'retention_days')::integer,     retention_days),
      max_daily_credits    = COALESCE((p_patch->>'max_daily_credits')::integer,  max_daily_credits),
      webhooks             = COALESCE(p_patch->'webhooks',                       webhooks),
      api_access_enabled   = COALESCE((p_patch->>'api_access_enabled')::boolean, api_access_enabled),
      metadata             = COALESCE(p_patch->'metadata',                       metadata),
      max_parallel_runs    = COALESCE((p_patch->>'max_parallel_runs')::integer,  max_parallel_runs),
      global_kill_switch   = COALESCE((p_patch->>'global_kill_switch')::boolean, global_kill_switch),
      agent_paused         = COALESCE((p_patch->>'agent_paused')::boolean,       agent_paused),
      dark_launch_enabled  = COALESCE((p_patch->>'dark_launch_enabled')::boolean,dark_launch_enabled),
      dark_launch_pct      = COALESCE((p_patch->>'dark_launch_pct')::integer,    dark_launch_pct),
      budget_enforce       = COALESCE((p_patch->>'budget_enforce')::boolean,     budget_enforce),
      updated_at           = now()
  WHERE ai_lenser_id = p_ai_lenser_id
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."lens_bindings" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "lens_id" "uuid" NOT NULL,
    "version_id" "uuid",
    "is_default" boolean DEFAULT false NOT NULL,
    "category_tags" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."lens_bindings" OWNER TO "postgres";


COMMENT ON TABLE "agents"."lens_bindings" IS 'Which lenses an AI Lenser may use in battles. version_id NULL means the agent always uses the current head version. Pin version_id for reproducible battle submissions.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_is_default" boolean DEFAULT true) RETURNS SETOF "agents"."lens_bindings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_profile_id uuid;
  v_binding_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT al.profile_id INTO v_profile_id
  FROM agents.ai_lensers al
  JOIN agents.ownerships o ON o.ai_lenser_id = al.id
  WHERE al.id = p_ai_lenser_id
    AND o.owner_lenser_id = v_caller_human_id
    AND o.role IN ('owner', 'co_owner')
    AND o.revoked_at IS NULL
  LIMIT 1;

  IF v_profile_id IS NULL THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.lenses l
    WHERE l.id = p_lens_id
      AND l.lenser_id = v_profile_id
  ) THEN
    RAISE EXCEPTION 'Main lens must be owned by the active AI workspace'
      USING ERRCODE = '42501';
  END IF;

  IF p_is_default THEN
    UPDATE agents.lens_bindings
    SET is_default = false
    WHERE ai_lenser_id = p_ai_lenser_id
      AND is_default = true;
  END IF;

  INSERT INTO agents.lens_bindings (ai_lenser_id, lens_id, version_id, is_default)
  VALUES (p_ai_lenser_id, p_lens_id, p_version_id, p_is_default)
  ON CONFLICT (ai_lenser_id, lens_id) DO UPDATE
    SET version_id = EXCLUDED.version_id,
        is_default = EXCLUDED.is_default
  RETURNING id INTO v_binding_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id,
    action_type,
    result,
    metadata
  ) VALUES (
    p_ai_lenser_id,
    'binding_updated',
    'success',
    jsonb_build_object(
      'binding_kind', 'lens',
      'binding_id', v_binding_id,
      'lens_id', p_lens_id,
      'version_id', p_version_id,
      'is_default', p_is_default
    )
  );

  RETURN QUERY
  SELECT lb.*
  FROM agents.lens_bindings lb
  WHERE lb.id = v_binding_id;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_is_default" boolean DEFAULT true, "p_category_tags" "text"[] DEFAULT '{}'::"text"[]) RETURNS SETOF "agents"."lens_bindings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_profile_id      uuid;
  v_binding_id      uuid;
  v_is_personality  boolean;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT al.profile_id INTO v_profile_id
  FROM agents.ai_lensers al
  JOIN agents.ownerships o ON o.ai_lenser_id = al.id
  WHERE al.id = p_ai_lenser_id
    AND o.owner_lenser_id = v_caller_human_id
    AND o.role IN ('owner', 'co_owner')
    AND o.revoked_at IS NULL
  LIMIT 1;

  IF v_profile_id IS NULL THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  -- Accept lenses that are:
  --   a) owned by the AI workspace profile, OR
  --   b) owned by the calling human co-owner, OR
  --   c) public + published (platform/system lenses accessible to everyone)
  IF NOT EXISTS (
    SELECT 1 FROM lenses.lenses l
    WHERE l.id = p_lens_id
      AND (
        l.lenser_id = v_profile_id
        OR l.lenser_id = v_caller_human_id
        OR (l.visibility = 'public' AND l.status = 'published')
      )
  ) THEN
    RAISE EXCEPTION 'Lens must be owned by the AI workspace, its co-owner, or be a public published lens'
      USING ERRCODE = '42501';
  END IF;

  v_is_personality := 'personality' = ANY(p_category_tags);

  -- Clear the default flag only within the same category scope
  IF p_is_default THEN
    IF v_is_personality THEN
      UPDATE agents.lens_bindings
        SET is_default = false
        WHERE ai_lenser_id = p_ai_lenser_id
          AND is_default = true
          AND 'personality' = ANY(category_tags);
    ELSE
      UPDATE agents.lens_bindings
        SET is_default = false
        WHERE ai_lenser_id = p_ai_lenser_id
          AND is_default = true
          AND NOT ('personality' = ANY(category_tags));
    END IF;
  END IF;

  INSERT INTO agents.lens_bindings
    (ai_lenser_id, lens_id, version_id, is_default, category_tags)
  VALUES
    (p_ai_lenser_id, p_lens_id, p_version_id, p_is_default, p_category_tags)
  ON CONFLICT (ai_lenser_id, lens_id) DO UPDATE
    SET version_id    = EXCLUDED.version_id,
        is_default    = EXCLUDED.is_default,
        category_tags = EXCLUDED.category_tags
  RETURNING id INTO v_binding_id;

  INSERT INTO agents.action_logs (ai_lenser_id, action_type, result, metadata)
  VALUES (
    p_ai_lenser_id,
    'binding_updated',
    'success',
    jsonb_build_object(
      'binding_kind',  'lens',
      'binding_id',    v_binding_id,
      'lens_id',       p_lens_id,
      'version_id',    p_version_id,
      'is_default',    p_is_default,
      'category_tags', p_category_tags
    )
  );

  RETURN QUERY SELECT lb.* FROM agents.lens_bindings lb WHERE lb.id = v_binding_id LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."model_bindings" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "model_id" "uuid" NOT NULL,
    "is_default" boolean DEFAULT false NOT NULL,
    "category_tags" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "byok_adapter" "text",
    "ollama_endpoint" "text",
    CONSTRAINT "model_bindings_byok_adapter_check" CHECK (("byok_adapter" = ANY (ARRAY['openai'::"text", 'anthropic'::"text", 'mistral'::"text", 'google'::"text", 'cohere'::"text", 'custom'::"text"])))
);


ALTER TABLE "agents"."model_bindings" OWNER TO "postgres";


COMMENT ON TABLE "agents"."model_bindings" IS 'Which AI models an AI Lenser is allowed to use. category_tags narrows selection by battle category. is_default selects the fallback when no category match exists.';



COMMENT ON COLUMN "agents"."model_bindings"."byok_adapter" IS 'Provider key for BYOK execution — keys are stored in execution.byok_keys, not here.';



COMMENT ON COLUMN "agents"."model_bindings"."ollama_endpoint" IS 'Local Ollama endpoint URL (e.g. http://localhost:11434). Never proxied to cloud.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean DEFAULT true) RETURNS SETOF "agents"."model_bindings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'ai'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_binding_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = v_caller_human_id
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM ai.models m
    WHERE m.id = p_model_id
      AND m.is_active = true
  ) THEN
    RAISE EXCEPTION 'Model not found or inactive'
      USING ERRCODE = 'P0001';
  END IF;

  IF p_is_default THEN
    UPDATE agents.model_bindings
    SET is_default = false
    WHERE ai_lenser_id = p_ai_lenser_id
      AND is_default = true;
  END IF;

  INSERT INTO agents.model_bindings (ai_lenser_id, model_id, is_default)
  VALUES (p_ai_lenser_id, p_model_id, p_is_default)
  ON CONFLICT (ai_lenser_id, model_id) DO UPDATE
    SET is_default = EXCLUDED.is_default
  RETURNING id INTO v_binding_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id,
    action_type,
    result,
    metadata
  ) VALUES (
    p_ai_lenser_id,
    'binding_updated',
    'success',
    jsonb_build_object(
      'binding_kind', 'model',
      'binding_id', v_binding_id,
      'model_id', p_model_id,
      'is_default', p_is_default
    )
  );

  RETURN QUERY
  SELECT mb.*
  FROM agents.model_bindings mb
  WHERE mb.id = v_binding_id;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[] DEFAULT '{}'::"text"[]) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "owner_lenser_id" "uuid", "role" "text", "permission_scope" "text"[], "granted_at" timestamp with time zone, "revoked_at" timestamp with time zone, "owner_handle" "text", "owner_display_name" "text", "owner_avatar_url" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_caller_human_id uuid := lensers.get_auth_human_lenser_id();
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'Not authorized to manage this AI Lenser.'
      USING ERRCODE = '42501';
  END IF;

  IF p_role NOT IN ('co_owner', 'operator') THEN
    RAISE EXCEPTION 'Only co_owner and operator may be granted through this RPC.'
      USING ERRCODE = '22023';
  END IF;

  IF p_owner_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Delegate owner_lenser_id is required.'
      USING ERRCODE = '22023';
  END IF;

  IF p_owner_lenser_id = v_caller_human_id THEN
    RAISE EXCEPTION 'The primary owner already has access and cannot be delegated through this RPC.'
      USING ERRCODE = '22023';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM agents.ai_lensers al
    WHERE al.id = p_ai_lenser_id
  ) THEN
    RAISE EXCEPTION 'AI Lenser not found.'
      USING ERRCODE = 'P0002';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lensers.profiles p
    WHERE p.id = p_owner_lenser_id
      AND p.type = 'human'
      AND p.status = 'active'
  ) THEN
    RAISE EXCEPTION 'Delegate must be an active human Lenser.'
      USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.ownerships (
    ai_lenser_id,
    owner_lenser_id,
    role,
    permission_scope,
    granted_at,
    revoked_at
  )
  VALUES (
    p_ai_lenser_id,
    p_owner_lenser_id,
    p_role,
    COALESCE(p_permission_scope, '{}'::text[]),
    now(),
    NULL
  )
  ON CONFLICT (ai_lenser_id, owner_lenser_id)
  DO UPDATE SET
    role = EXCLUDED.role,
    permission_scope = EXCLUDED.permission_scope,
    granted_at = now(),
    revoked_at = NULL;

  RETURN QUERY
  SELECT
    o.id,
    o.ai_lenser_id,
    o.owner_lenser_id,
    o.role,
    o.permission_scope,
    o.granted_at,
    o.revoked_at,
    p.handle,
    p.display_name,
    p.avatar_url
  FROM agents.ownerships o
  JOIN lensers.profiles p
    ON p.id = o.owner_lenser_id
  WHERE o.ai_lenser_id = p_ai_lenser_id
    AND o.owner_lenser_id = p_owner_lenser_id
  LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) IS 'Creates or updates a co_owner/operator delegate for an AI Lenser. owner transfers stay out of scope for this RPC.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text" DEFAULT NULL::"text", "p_recent_output_summary" "text" DEFAULT NULL::"text", "p_blocker_summary" "text" DEFAULT NULL::"text", "p_started_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_existing_id uuid;
  v_result      jsonb;
BEGIN
  SELECT id INTO v_existing_id
  FROM agents.agent_run_steps
  WHERE team_run_id = p_team_run_id AND workflow_node_id = p_workflow_node_id
  LIMIT 1;

  IF v_existing_id IS NOT NULL THEN
    UPDATE agents.agent_run_steps
    SET status                = p_status,
        title                 = p_title,
        current_task          = COALESCE(p_current_task, current_task),
        recent_output_summary = COALESCE(p_recent_output_summary, recent_output_summary),
        blocker_summary       = COALESCE(p_blocker_summary, blocker_summary),
        completed_at          = COALESCE(p_completed_at, completed_at),
        updated_at            = now()
    WHERE id = v_existing_id
    RETURNING to_jsonb(agents.agent_run_steps.*) INTO v_result;
  ELSE
    INSERT INTO agents.agent_run_steps
      (team_run_id, workflow_node_id, lane, title, status,
       current_task, recent_output_summary, blocker_summary,
       started_at, completed_at)
    VALUES
      (p_team_run_id, p_workflow_node_id, p_lane, p_title, p_status,
       p_current_task, p_recent_output_summary, p_blocker_summary,
       COALESCE(p_started_at, now()), p_completed_at)
    RETURNING to_jsonb(agents.agent_run_steps.*) INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid" DEFAULT NULL::"uuid", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_ref_id" "uuid" DEFAULT NULL::"uuid", "p_max_tokens" integer DEFAULT 4096, "p_temperature" numeric DEFAULT 0.70) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  -- Ensure caller owns the battle
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
    WHERE b.id = p_battle_id
      AND b.creator_lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'upsert_battle_execution_config_forbidden' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.execution_configs
    (battle_id, contender_id, provider_key, model_key, model_id, funding_source,
     byok_key_ref_id, max_tokens, temperature)
  VALUES
    (p_battle_id, p_contender_id, p_provider_key, p_model_key, p_model_id,
     p_funding_source, p_byok_key_ref_id, p_max_tokens, p_temperature)
  ON CONFLICT (battle_id, contender_id)
  DO UPDATE SET
    provider_key   = EXCLUDED.provider_key,
    model_key      = EXCLUDED.model_key,
    model_id       = EXCLUDED.model_id,
    funding_source = EXCLUDED.funding_source,
    byok_key_ref_id = EXCLUDED.byok_key_ref_id,
    max_tokens     = EXCLUDED.max_tokens,
    temperature    = EXCLUDED.temperature,
    updated_at     = now()
  RETURNING to_jsonb(battles.execution_configs.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  INSERT INTO public.notification_preferences (lenser_id, notification_type, enabled)
  VALUES (lensers.get_auth_lenser_id(), p_type, p_enabled)
  ON CONFLICT (lenser_id, notification_type)
  DO UPDATE SET enabled = p_enabled, updated_at = now();
END;
$$;


ALTER FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text" DEFAULT NULL::"text", "p_status" "text" DEFAULT 'unconfigured'::"text", "p_ai_key_id" "uuid" DEFAULT NULL::"uuid") RETURNS "agents"."provider_configs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.provider_configs;
BEGIN
  IF p_status NOT IN ('healthy', 'error', 'unconfigured') THEN
    RAISE EXCEPTION 'invalid status value: %', p_status USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.provider_configs (
    ai_lenser_id,
    provider_key,
    ai_key_id,
    base_url,
    status,
    configured_at,
    last_checked_at,
    updated_at
  )
  VALUES (
    p_ai_lenser_id,
    p_provider_key,
    p_ai_key_id,
    p_base_url,
    p_status,
    -- configured_at: set on first key association; preserved on subsequent updates
    CASE WHEN p_ai_key_id IS NOT NULL THEN now() ELSE NULL END,
    -- last_checked_at: set whenever a health check result (healthy/error) is recorded
    CASE WHEN p_status IN ('healthy', 'error') THEN now() ELSE NULL END,
    now()
  )
  ON CONFLICT (ai_lenser_id, provider_key) DO UPDATE
    SET ai_key_id       = COALESCE(EXCLUDED.ai_key_id,   provider_configs.ai_key_id),
        base_url        = COALESCE(EXCLUDED.base_url,    provider_configs.base_url),
        status          = EXCLUDED.status,
        configured_at   = CASE
                            WHEN EXCLUDED.ai_key_id IS NOT NULL
                              AND provider_configs.configured_at IS NULL
                            THEN now()
                            ELSE provider_configs.configured_at
                          END,
        last_checked_at = CASE
                            WHEN EXCLUDED.status IN ('healthy', 'error')
                            THEN now()
                            ELSE provider_configs.last_checked_at
                          END,
        updated_at      = now()
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") IS 'Upserts a provider config row. ai_key_id: COALESCE-preserves existing key reference if not passed (health-check updates). base_url: COALESCE-preserves existing value if not passed. configured_at: set only on first key association (not overwritten on re-health-check). last_checked_at: updated whenever status is healthy or error. Called by authenticated users (after fn_store_api_key) and by test-provider Edge Function (service_role).';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "source_node_id" "uuid", "target_node_id" "uuid", "source_output_key" "text", "target_param_label" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller does not own workflow %', p_workflow_id
      USING ERRCODE = '42501';
  END IF;

  -- Full replace: delete all existing edges for this workflow
  DELETE FROM lenses.workflow_edges
  WHERE lenses.workflow_edges.workflow_id = p_workflow_id;

  -- Insert incoming edges (skip if array is empty)
  IF jsonb_array_length(p_edges) > 0 THEN
    INSERT INTO lenses.workflow_edges (
      id,
      workflow_id,
      source_node_id,
      target_node_id,
      source_output_key,
      target_param_label
    )
    SELECT
      COALESCE((e->>'id')::uuid, gen_random_uuid()),
      p_workflow_id,
      (e->>'source_node_id')::uuid,
      (e->>'target_node_id')::uuid,
      COALESCE(e->>'source_output_key', 'output'),
      e->>'target_param_label'
    FROM jsonb_array_elements(p_edges) AS e;
  END IF;

  -- Return ALL edges for the workflow (not just the ones we inserted)
  RETURN QUERY
    SELECT
      we.id,
      we.workflow_id,
      we.source_node_id,
      we.target_node_id,
      we.source_output_key,
      we.target_param_label
    FROM lenses.workflow_edges we
    WHERE we.workflow_id = p_workflow_id;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") IS 'Bulk-upserts workflow edges for a workflow the caller owns.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "position_x" double precision, "position_y" double precision, "label" "text", "ordinal" integer, "config" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller does not own workflow %', p_workflow_id
      USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO lenses.workflow_nodes (
    id,
    workflow_id,
    lens_id,
    version_id,
    position_x,
    position_y,
    label,
    ordinal,
    config
  )
  SELECT
    COALESCE((n->>'id')::uuid, gen_random_uuid()),
    p_workflow_id,
    (n->>'lens_id')::uuid,
    (n->>'version_id')::uuid,
    COALESCE((n->>'position_x')::float, 0),
    COALESCE((n->>'position_y')::float, 0),
    n->>'label',
    COALESCE((n->>'ordinal')::integer, 0),
    COALESCE(n->'config', '{}'::jsonb)
  FROM jsonb_array_elements(p_nodes) AS n
  ON CONFLICT ON CONSTRAINT workflow_nodes_pkey DO UPDATE SET
    position_x = EXCLUDED.position_x,
    position_y = EXCLUDED.position_y,
    label      = EXCLUDED.label,
    ordinal    = EXCLUDED.ordinal,
    version_id = EXCLUDED.version_id,
    config     = EXCLUDED.config
  RETURNING
    lenses.workflow_nodes.id,
    lenses.workflow_nodes.workflow_id,
    lenses.workflow_nodes.lens_id,
    lenses.workflow_nodes.version_id,
    lenses.workflow_nodes.position_x,
    lenses.workflow_nodes.position_y,
    lenses.workflow_nodes.label,
    lenses.workflow_nodes.ordinal,
    lenses.workflow_nodes.config,
    lenses.workflow_nodes.created_at;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") IS 'Bulk-upserts workflow nodes for a workflow the caller owns. p_nodes is a JSON array matching UpsertNodeInput shape. Now includes config JSONB for per-node model and parameter overrides.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_ordinal" integer DEFAULT 0, "p_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'upsert_workflow_phase_forbidden' USING ERRCODE = '42501';
  END IF;

  IF p_id IS NOT NULL THEN
    UPDATE lenses.workflow_phases
    SET title = p_title, description = p_description, ordinal = p_ordinal, updated_at = now()
    WHERE id = p_id AND workflow_id = p_workflow_id
    RETURNING to_jsonb(lenses.workflow_phases.*) INTO v_result;
  END IF;

  IF v_result IS NULL THEN
    INSERT INTO lenses.workflow_phases (workflow_id, title, description, ordinal)
    VALUES (p_workflow_id, p_title, p_description, p_ordinal)
    RETURNING to_jsonb(lenses.workflow_phases.*) INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid" DEFAULT NULL::"uuid", "p_cron_expr" "text" DEFAULT '* * * * *'::"text", "p_timezone" "text" DEFAULT 'UTC'::"text", "p_description" "text" DEFAULT NULL::"text", "p_approval_policy" "jsonb" DEFAULT '{"requiresApproval": true}'::"jsonb", "p_is_active" boolean DEFAULT true, "p_global_model_id" "text" DEFAULT NULL::"text", "p_assignee_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_assignment_id" "uuid" DEFAULT NULL::"uuid", "p_retry_policy" "jsonb" DEFAULT '{"maxRetries": 1}'::"jsonb", "p_failure_policy" "jsonb" DEFAULT '{"mode": "isolate"}'::"jsonb", "p_queue_policy" "jsonb" DEFAULT '{"mode": "parallel"}'::"jsonb", "p_inputs_template" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "cron_expr" "text", "timezone" "text", "description" "text", "is_active" boolean, "assignee_type" "text", "assignee_id" "uuid", "workflow_assignment_id" "uuid", "approval_policy" "jsonb", "retry_policy" "jsonb", "failure_policy" "jsonb", "queue_policy" "jsonb", "inputs_template" "jsonb", "global_model_id" "text", "next_run_at" timestamp with time zone, "last_run_at" timestamp with time zone, "last_run_id" "uuid", "last_dispatch_status" "text", "last_error_at" timestamp with time zone, "last_error_message" "text", "last_completed_at" timestamp with time zone, "last_result" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_owner_id          uuid;
  v_active_id         uuid := lensers.get_auth_lenser_id();
  v_human_id          uuid := lensers.get_auth_human_lenser_id();
  v_ai_lenser_id      uuid;
  v_parts             text[];
  v_requires_approval boolean;
  v_spending_limit    numeric;
BEGIN
  -- Resolve workflow owner (human profile id)
  SELECT w.lenser_id INTO v_owner_id
  FROM lenses.workflows w
  WHERE w.id = p_workflow_id;

  -- Authorize when:
  --   a) Active session lenser owns the workflow (legacy human session), OR
  --   b) Human profile resolved from active session owns the workflow
  --      (agent workspace active; human still owns the workflow), OR
  --   c) Caller is an admin.
  IF v_owner_id IS NULL
     OR (v_owner_id <> v_active_id
         AND (v_human_id IS NULL OR v_owner_id <> v_human_id)
         AND NOT public.is_admin())
  THEN
    RAISE EXCEPTION 'not the owner of workflow %', p_workflow_id
      USING ERRCODE = '42501';
  END IF;

  -- CRON validation
  v_parts := regexp_split_to_array(trim(COALESCE(p_cron_expr, '')), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RAISE EXCEPTION 'Invalid CRON expression "%" — expected 5 fields', p_cron_expr
      USING ERRCODE = '22023';
  END IF;

  -- Cycle detection
  IF p_is_active AND lenses.fn_workflow_has_cycle(p_workflow_id) THEN
    RAISE EXCEPTION 'Cannot activate schedule for a workflow with cycles'
      USING ERRCODE = '22023', DETAIL = 'cycle_detected';
  END IF;

  -- Approval-bypass guard + audit (unchanged)
  IF p_is_active THEN
    v_requires_approval := COALESCE(
      (p_approval_policy->>'requiresApproval')::boolean,
      true
    );

    IF NOT v_requires_approval THEN
      SELECT al.id, pol.spending_limit_credits
        INTO v_ai_lenser_id, v_spending_limit
      FROM agents.ai_lensers al
      JOIN agents.policies pol ON pol.id = al.policy_id
      WHERE al.profile_id = v_owner_id
      LIMIT 1;

      IF v_spending_limit IS NULL THEN
        RAISE EXCEPTION
          'Cannot activate a schedule with requiresApproval=false and no spending_limit_credits. '
          'Set a spending limit on the agent policy before disabling approval gates.'
          USING ERRCODE = '23514';
      END IF;

      IF v_ai_lenser_id IS NOT NULL THEN
        INSERT INTO agents.action_logs (ai_lenser_id, action_type, result, metadata)
        VALUES (
          v_ai_lenser_id,
          'approval_bypass_attempted',
          'recorded',
          jsonb_build_object(
            'workflow_id',    p_workflow_id,
            'schedule_id',    p_schedule_id,
            'actor_lenser_id', v_owner_id,
            'spending_limit', v_spending_limit
          )
        );
      END IF;
    END IF;
  END IF;

  RETURN QUERY
  SELECT * FROM lenses.fn_upsert_workflow_schedule_internal(
    p_workflow_id, p_schedule_id, p_cron_expr, p_timezone, p_description,
    p_approval_policy, p_is_active, p_global_model_id, p_assignee_id,
    p_workflow_assignment_id, p_retry_policy, p_failure_policy,
    p_queue_policy, p_inputs_template
  );
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") IS 'Upsert a workflow schedule. Authorizes the active session lenser OR the resolved human owner OR an admin so the call succeeds after workspace switches.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text" DEFAULT NULL::"text", "p_output_type" "text" DEFAULT 'text'::"text", "p_model_hint" "text" DEFAULT NULL::"text", "p_ordinal" integer DEFAULT 0, "p_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'upsert_workflow_task_forbidden' USING ERRCODE = '42501';
  END IF;

  IF p_id IS NOT NULL THEN
    UPDATE lenses.workflow_tasks
    SET title = p_title, prompt_text = p_prompt_text, output_type = p_output_type,
        model_hint = p_model_hint, ordinal = p_ordinal, updated_at = now()
    WHERE id = p_id AND phase_id = p_phase_id
    RETURNING to_jsonb(lenses.workflow_tasks.*) INTO v_result;
  END IF;

  IF v_result IS NULL THEN
    INSERT INTO lenses.workflow_tasks
      (phase_id, workflow_id, title, prompt_text, output_type, model_hint, ordinal)
    VALUES (p_phase_id, p_workflow_id, p_title, p_prompt_text, p_output_type, p_model_hint, p_ordinal)
    RETURNING to_jsonb(lenses.workflow_tasks.*) INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $_$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
  v_predicate text;
  v_result    jsonb;
BEGIN
  IF p_table_name NOT IN (
    'teams', 'team_members', 'team_edges', 'personality_profiles',
    'memory_profiles', 'tool_profiles', 'model_profiles',
    'workflow_assignments', 'scratchpad_runs', 'workspace_settings',
    'evaluation_cases', 'evaluation_rubrics', 'evaluation_baselines',
    'tools_registry', 'tool_assignments', 'evaluations',
    'agent_run_events', 'agent_run_steps'
  ) THEN
    RAISE EXCEPTION 'upsert_forbidden: table % not in allowlist', p_table_name
      USING ERRCODE = '42501';
  END IF;

  v_predicate := public.fn__workspace_ownership_predicate(p_table_name);
  IF v_predicate IS NULL THEN
    RAISE EXCEPTION 'upsert_forbidden: no ownership predicate for %', p_table_name
      USING ERRCODE = '42501';
  END IF;

  EXECUTE format(
    'UPDATE agents.%I tbl SET updated_at = now() '
    'WHERE tbl.id = $1 AND (%s OR public.is_admin()) '
    'RETURNING to_jsonb(tbl.*)',
    p_table_name,
    v_predicate
  ) USING p_id, v_lenser_id INTO v_result;

  IF v_result IS NULL THEN
    RAISE EXCEPTION 'update_not_found_or_forbidden: record % in table % not found or not owned',
      p_id, p_table_name USING ERRCODE = '42501';
  END IF;

  RETURN v_result;
END;
$_$;


ALTER FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") IS 'Update any agents workspace record by id. Table name is whitelisted. Ownership enforced via human lenser id resolved through agents.ownerships (works after workspace switching). No reference to non-existent owner_lenser_id column on agent tables.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  RETURN battles.fn_move_battle_job_to_dlq(p_job_id, p_error_code, p_error_msg);
END;
$$;


ALTER FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") RETURNS TABLE("job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "task_prompt" "text", "provider_key" "text", "model_key" "text", "byok_key_ref_id" "text", "lens_id" "uuid", "version_id" "uuid", "max_tokens" integer, "temperature" numeric, "retry_count" integer, "ai_lenser_id" "uuid", "personality_note" "text", "personality_version_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM battles.fn_claim_battle_execution_job(p_worker_id);
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_queued_run"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  SELECT to_jsonb(r.*) INTO v_result
  FROM execution.fn_claim_queued_run() r;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_queued_run"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_claim_queued_run"() IS 'Worker-only: claim the next queued execution run. Returns the claimed run as jsonb.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "inputs" "jsonb", "model_id" "uuid", "ai_lenser_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM lenses.fn_claim_scheduled_workflow_run(p_worker_id);
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") IS 'Worker-only: claim the next pending scheduled workflow run. Delegates to lenses.fn_claim_scheduled_workflow_run.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_team_run"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run jsonb;
BEGIN
  UPDATE agents.team_runs
  SET status     = 'running',
      started_at = COALESCE(started_at, now()),
      updated_at = now()
  WHERE id = (
    SELECT id FROM agents.team_runs
    WHERE status = 'pending'
    ORDER BY created_at
    LIMIT 1
    FOR UPDATE SKIP LOCKED
  )
  RETURNING to_jsonb(agents.team_runs.*) INTO v_run;

  RETURN v_run;
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_team_run"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text" DEFAULT NULL::"text", "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_complete_battle_execution_job(p_job_id, p_status, p_output_text, p_error);
END;
$$;


ALTER FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer DEFAULT NULL::integer, "p_token_output" integer DEFAULT NULL::integer, "p_credit_cost" bigint DEFAULT NULL::bigint, "p_billing_status" "text" DEFAULT NULL::"text", "p_response_text" "text" DEFAULT NULL::"text", "p_response_meta" "jsonb" DEFAULT NULL::"jsonb", "p_error_code" "text" DEFAULT NULL::"text", "p_error_message" "text" DEFAULT NULL::"text", "p_latency_ms" integer DEFAULT NULL::integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
BEGIN
  PERFORM execution.fn_complete_execution_run(
    p_run_id, p_status, p_token_input, p_token_output, p_credit_cost,
    p_billing_status, p_response_text, p_response_meta,
    p_error_code, p_error_message, p_latency_ms
  );
END;
$$;


ALTER FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) IS 'Worker-only: mark an execution run terminal (completed or failed). Delegates to execution.fn_complete_execution_run.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
DECLARE
  v_result text;
BEGIN
  SELECT ai.fn_decrypt_api_key(p_key_id) INTO v_result;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") IS 'Worker-only: decrypt a stored BYOK API key. Delegates to ai.fn_decrypt_api_key.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text" DEFAULT 'provider_failed'::"text", "p_error_message" "text" DEFAULT 'Provider reported failure'::"text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
  UPDATE execution.runs
  SET    status        = 'failed',
         completed_at  = now(),
         error_code    = p_error_code,
         error_message = p_error_message
  WHERE  id = p_run_id;
$$;


ALTER FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") IS 'Worker-only: mark an execution run as failed with an error code and message.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai', 'vault', 'lensers'
    AS $$
DECLARE
  v_lenser_id    uuid;
  v_encrypted_id uuid;
  v_decrypted    text;
BEGIN
  IF p_ai_key_id IS NULL OR p_user_id IS NULL THEN
    RAISE EXCEPTION 'p_ai_key_id and p_user_id are required';
  END IF;

  -- Resolve the human profile for the authenticated user; ai.keys are owned
  -- by the human profile (not an agent workspace profile).
  SELECT p.id INTO v_lenser_id
  FROM lensers.profiles p
  WHERE p.user_id = p_user_id
    AND p.type = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Key owner profile not found';
  END IF;

  -- Ownership check: key must belong to caller AND be active.
  SELECT k.encrypted_key_id INTO v_encrypted_id
  FROM ai.keys k
  WHERE k.id = p_ai_key_id
    AND k.lenser_id = v_lenser_id
    AND k.is_active = true
  LIMIT 1;

  IF v_encrypted_id IS NULL THEN
    -- Generic message: do not disclose whether the key exists for another user.
    RAISE EXCEPTION 'Key not found, revoked, or not owned by caller';
  END IF;

  SELECT decrypted_secret INTO v_decrypted
  FROM vault.decrypted_secrets
  WHERE id = v_encrypted_id;

  IF v_decrypted IS NULL THEN
    RAISE EXCEPTION 'Failed to decrypt key from vault';
  END IF;

  RETURN v_decrypted;
END;
$$;


ALTER FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") IS 'Worker-only: resolve and decrypt a BYOK API key from ai.keys via vault.decrypted_secrets. Requires p_user_id (the authenticated caller''s auth.uid()) and verifies the key belongs to that user. Patched 2026-05-16 to close cross-user IDOR.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "title" "text", "task_prompt" "text", "ai_judge_model_key" "text", "ai_judge_prompt" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT
    b.id,
    b.title,
    b.task_prompt,
    b.ai_judge_model_key,
    b.ai_judge_prompt
  FROM battles.battles b
  WHERE b.id = p_battle_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") IS 'CU: Returns battle fields required by the ai-judge-battle edge function. service_role only — replaces the invalid direct PostgREST table access that was silently returning empty results because the battles schema is not REST-exposed.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "task_prompt" "text", "battle_type" "text", "total_vote_count" integer, "finalized_at" timestamp with time zone, "winner_contender_id" "uuid", "published_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT b.id, b.slug, b.title, b.status, b.task_prompt, b.battle_type,
         b.total_vote_count, b.finalized_at, b.winner_contender_id, b.published_at
  FROM battles.battles b
  WHERE b.id = p_battle_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT jsonb_build_object(
    'team_run', to_jsonb(tr.*),
    'team',     to_jsonb(t.*),
    'members',  COALESCE((
      SELECT jsonb_agg(to_jsonb(tm.*) ORDER BY tm.sort_order)
      FROM agents.team_members tm WHERE tm.team_id = tr.team_id
    ), '[]'::jsonb)
  )
  FROM agents.team_runs tr
  LEFT JOIN agents.teams t ON t.id = tr.team_id
  WHERE tr.id = p_team_run_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_body text;
BEGIN
  IF p_version_id IS NOT NULL THEN
    SELECT v.template_body INTO v_body
    FROM lenses.versions v
    WHERE v.id = p_version_id
      AND v.lens_id = p_lens_id
    LIMIT 1;
    RETURN v_body;
  END IF;

  SELECT v.template_body INTO v_body
  FROM lenses.versions v
  WHERE v.lens_id = p_lens_id
    AND v.status = 'published'::content.content_status
  ORDER BY v.published_at DESC NULLS LAST, v.version_number DESC
  LIMIT 1;

  IF v_body IS NULL THEN
    SELECT v.template_body INTO v_body
    FROM lenses.versions v
    WHERE v.lens_id = p_lens_id
    ORDER BY v.version_number DESC
    LIMIT 1;
  END IF;

  RETURN v_body;
END;
$$;


ALTER FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") IS 'Worker-only: returns lenses.versions.template_body for workflow DAG execution (raw [[placeholders]]). When p_version_id is NULL, prefers latest published version for the lens.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT to_jsonb(tr.*)
  FROM agents.team_runs tr
  WHERE tr.id = p_team_run_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'reputation'
    AS $$
  SELECT jsonb_build_object(
    'battle_status',  b.status,
    'vote_count',     (
      SELECT COUNT(*)
      FROM battles.votes v
      WHERE v.battle_id = p_battle_id
    ),
    'lenser_score',   (
      SELECT ls.score
      FROM reputation.lenser_scores ls
      WHERE ls.lenser_id  = p_lenser_id
        AND ls.score_type = 'composite'
      ORDER BY ls.computed_at DESC
      LIMIT 1
    ),
    'lenser_elo',     (
      SELECT cr.elo_rating
      FROM reputation.contender_ratings cr
      WHERE cr.lenser_id = p_lenser_id
      ORDER BY cr.updated_at DESC
      LIMIT 1
    )
  )
  FROM battles.battles b
  WHERE b.id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") IS 'Worker-only: return battle status, total vote count, and lenser scores as jsonb for the score-vote-risk edge function. Single round-trip.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) RETURNS TABLE("recent_count" bigint, "total_count" bigint, "draw_count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT
    COUNT(*) FILTER (WHERE v.created_at >= p_since_ts) AS recent_count,
    COUNT(*)                                            AS total_count,
    COUNT(*) FILTER (WHERE v.is_draw = true)            AS draw_count
  FROM battles.votes v
  WHERE v.voter_lenser_id = p_voter_lenser_id;
$$;


ALTER FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) IS 'Worker-only: return recent/total/draw vote counts for a voter in one query.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") RETURNS TABLE("workflow_id" "uuid", "triggered_by" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT wr.workflow_id, wr.triggered_by
  FROM lenses.workflow_runs wr
  WHERE wr.id = p_run_id;
$$;


ALTER FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") IS 'Worker-only: return workflow_id and triggered_by for a workflow run.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT jsonb_build_object(
    'nodes', COALESCE(
      (SELECT jsonb_agg(
         jsonb_build_object(
           'id',         n.id,
           'lens_id',    n.lens_id,
           'version_id', n.version_id,
           'config',     n.config
         ) ORDER BY n.ordinal
       )
       FROM lenses.workflow_nodes n
       WHERE n.workflow_id = p_workflow_id
      ), '[]'::jsonb
    ),
    'edges', COALESCE(
      (SELECT jsonb_agg(
         jsonb_build_object(
           'id',                e.id,
           'source_node_id',    e.source_node_id,
           'target_node_id',    e.target_node_id,
           'source_output_key', e.source_output_key,
           'target_param_label',e.target_param_label
         )
       )
       FROM lenses.workflow_edges e
       WHERE e.workflow_id = p_workflow_id
      ), '[]'::jsonb
    )
  );
$$;


ALTER FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") IS 'Worker-only: return workflow nodes and edges as { nodes: [...], edges: [...] }. Reduces two round-trips to one.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'media'
    AS $$
  INSERT INTO media.objects
    (workspace_id, owner_lenser_id, external_url, mime_type, media_type,
     name, visibility, lifecycle_state, metadata)
  VALUES (
    p_workspace_id, p_owner_lenser_id, p_external_url, p_mime_type, p_media_type,
    p_name, 'private', 'active',
    jsonb_build_object('workflow_run_id', p_run_id, 'node_id', p_node_id)
  )
  RETURNING id;
$$;


ALTER FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") IS 'Worker-only: insert a media.objects row for output produced by a workflow node. Tags the object with workflow_run_id and node_id in metadata.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text" DEFAULT NULL::"text", "p_content_json" "jsonb" DEFAULT NULL::"jsonb", "p_media_ids" "uuid"[] DEFAULT NULL::"uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
BEGIN
  PERFORM execution.fn_persist_execution_artifacts(
    p_run_id, p_lenser_id, p_workspace_id, p_ai_model_id,
    p_kind, p_content_text, p_content_json, p_media_ids
  );
END;
$$;


ALTER FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) IS 'Worker-only: persist execution artifacts to the DB. Delegates to execution.fn_persist_execution_artifacts.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
DECLARE
  v_result text;
BEGIN
  SELECT lenses.fn_render_template(p_version_id, p_inputs) INTO v_result;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") IS 'Worker-only: render a lens version template with the supplied inputs. Delegates to lenses.fn_render_template.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_requeue_battle_job_with_backoff(p_job_id, p_backoff_ms, p_error);
END;
$$;


ALTER FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_run_auto_promote_cycle"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  r         RECORD;
  promoted  integer := 0;
BEGIN
  FOR r IN
    SELECT id
    FROM   battles.battles
    WHERE  status      = 'draft'
      AND  auto_promote = TRUE
  LOOP
    BEGIN
      IF public.fn_battles_auto_promote(r.id) THEN
        promoted := promoted + 1;
      END IF;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_worker_run_auto_promote_cycle: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN promoted;
END;
$$;


ALTER FUNCTION "public"."fn_worker_run_auto_promote_cycle"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() IS 'CU: Wraps the auto-promote sweep for the Node.js worker. Replaces the direct svc.from(''battles'') query that targeted public.battles (which does not exist; the table is in battles.battles, not REST-exposed). Returns the number of battles promoted draft → open this cycle.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  UPDATE battles.battles
  SET og_image_url = p_og_image_url
  WHERE id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") IS 'Worker-only: set og_image_url on a battle. Called by the generate-battle-og-image edge function.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid" DEFAULT NULL::"uuid", "p_byok_key_ref_id" "uuid" DEFAULT NULL::"uuid", "p_input_snapshot" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("request_id" "uuid", "run_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'execution', 'tenancy'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_user_id IS NULL THEN
    RAISE EXCEPTION 'p_user_id is required';
  END IF;

  -- Resolve the human profile for this auth user.
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = p_user_id
    AND type    = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'No human lenser profile found for user';
  END IF;

  RETURN QUERY
    SELECT r.request_id, r.run_id
    FROM execution.fn_start_execution(
      p_lenser_id       => v_lenser_id,
      p_origin_type     => p_origin_type,
      p_funding_source  => p_funding_source,
      p_model_id        => NULL,
      p_lens_id         => p_lens_id,
      p_byok_key_ref_id => p_byok_key_ref_id,
      p_input_snapshot  => p_input_snapshot
    ) r;
END;
$$;


ALTER FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") IS 'Edge-function gateway: resolves auth user → lenser profile, then delegates to execution.fn_start_execution. Accepts p_user_id so the edge function never touches private schemas through PostgREST. Callable only by service_role.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  UPDATE agents.team_runs
  SET status       = p_status,
      completed_at = COALESCE(p_completed_at, CASE WHEN p_status IN ('completed','failed','cancelled') THEN now() ELSE NULL END),
      updated_at   = now()
  WHERE id = p_team_run_id;
$$;


ALTER FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT public.fn_worker_update_team_run(p_team_run_id, p_status, p_completed_at);
$$;


ALTER FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  UPDATE reputation.vote_risk_scores
  SET risk_score    = p_risk_score,
      risk_factors  = p_risk_factors,
      review_status = p_review_status
  WHERE vote_id = p_vote_id;
$$;


ALTER FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") IS 'Worker-only: update risk_score, risk_factors, and review_status on a vote_risk_scores row.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text" DEFAULT NULL::"text", "p_execution_run_id" "uuid" DEFAULT NULL::"uuid", "p_artifact_id" "uuid" DEFAULT NULL::"uuid", "p_is_final" boolean DEFAULT true, "p_media_url" "text" DEFAULT NULL::"text", "p_mime_type" "text" DEFAULT NULL::"text", "p_output_modality" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
DECLARE
  v_status        text;
  v_content_url   text;
  v_submitted_at  timestamptz;
BEGIN
  -- Determine status and whether we have a final value to submit.
  -- Async-pending: media_url is NULL (task not yet polled), artifact_id holds
  -- the providerTaskId for the poll worker.
  IF p_content_text IS NOT NULL OR p_media_url IS NOT NULL THEN
    v_status       := 'submitted';
    v_submitted_at := now();
    -- Mirror media_url into content_url to satisfy submissions_content_required.
    v_content_url  := p_media_url;
  ELSE
    -- No content yet: async media task is pending.
    v_status       := 'pending';
    v_submitted_at := NULL;
    v_content_url  := NULL;
  END IF;

  INSERT INTO battles.submissions (
    battle_id,
    contender_id,
    content_text,
    content_url,
    media_url,
    mime_type,
    output_modality,
    execution_run_id,
    artifact_id,
    is_final,
    status,
    source_type,
    submitted_at
  ) VALUES (
    p_battle_id,
    p_contender_id,
    p_content_text,
    v_content_url,
    p_media_url,
    p_mime_type,
    COALESCE(p_output_modality, CASE WHEN p_content_text IS NOT NULL THEN 'text' ELSE NULL END),
    p_execution_run_id,
    p_artifact_id,
    p_is_final,
    v_status,
    'execution_output',
    v_submitted_at
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE
    SET content_text      = COALESCE(EXCLUDED.content_text,      battles.submissions.content_text),
        content_url       = COALESCE(EXCLUDED.content_url,       battles.submissions.content_url),
        media_url         = COALESCE(EXCLUDED.media_url,         battles.submissions.media_url),
        mime_type         = COALESCE(EXCLUDED.mime_type,         battles.submissions.mime_type),
        output_modality   = COALESCE(EXCLUDED.output_modality,   battles.submissions.output_modality),
        execution_run_id  = COALESCE(EXCLUDED.execution_run_id,  battles.submissions.execution_run_id),
        artifact_id       = COALESCE(EXCLUDED.artifact_id,       battles.submissions.artifact_id),
        is_final          = EXCLUDED.is_final,
        status            = EXCLUDED.status,
        submitted_at      = COALESCE(battles.submissions.submitted_at, EXCLUDED.submitted_at),
        updated_at        = now();
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") IS 'Worker-only (service_role): upsert a battle submission for an AI contender. Handles text, sync media (image/audio), and async-pending media (video/music) results. Does not check auth.uid() — callers must run as service_role.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'platform'
    AS $$
BEGIN
  PERFORM platform.fn_upsert_worker_heartbeat(p_worker_id, p_worker_type, p_metadata);
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") IS 'Worker-only: upsert a worker heartbeat record. Delegates to platform.fn_upsert_worker_heartbeat.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
DECLARE
  v_terminal text[] := ARRAY['completed', 'failed', 'timed_out', 'cancelled'];
BEGIN
  INSERT INTO lenses.workflow_node_results
    (run_id, node_id, status, output_data, error_message, started_at, completed_at)
  VALUES (
    p_run_id, p_node_id, p_status, p_output_data, p_error_message,
    CASE WHEN p_status = 'running' THEN now() ELSE NULL END,
    CASE WHEN p_status = ANY(v_terminal) THEN now() ELSE NULL END
  )
  ON CONFLICT (run_id, node_id) DO UPDATE
    SET status        = EXCLUDED.status,
        output_data   = COALESCE(EXCLUDED.output_data,   lenses.workflow_node_results.output_data),
        error_message = COALESCE(EXCLUDED.error_message, lenses.workflow_node_results.error_message),
        started_at    = COALESCE(lenses.workflow_node_results.started_at, EXCLUDED.started_at),
        completed_at  = CASE
          WHEN EXCLUDED.status = ANY(v_terminal) THEN now()
          ELSE lenses.workflow_node_results.completed_at
        END;
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") IS 'Worker-only: upsert a workflow node execution result. Sets started_at on first "running" write; sets completed_at on terminal status. Preserves existing output_data/error_message if new value is NULL.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text", "p_resolved_input_snapshot" "jsonb" DEFAULT NULL::"jsonb", "p_provider_route" "jsonb" DEFAULT NULL::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
DECLARE
  v_terminal text[] := ARRAY['completed', 'failed', 'timed_out', 'cancelled', 'blocked', 'invalidated', 'skipped'];
BEGIN
  INSERT INTO lenses.workflow_node_results (
    run_id, node_id, status, output_data, error_message,
    resolved_input_snapshot, provider_route,
    started_at, completed_at
  )
  VALUES (
    p_run_id, p_node_id, p_status, p_output_data, p_error_message,
    p_resolved_input_snapshot, p_provider_route,
    CASE WHEN p_status IN ('running', 'streaming', 'retrying') THEN now() ELSE NULL END,
    CASE WHEN p_status = ANY(v_terminal) THEN now() ELSE NULL END
  )
  ON CONFLICT (run_id, node_id) DO UPDATE
    SET status        = EXCLUDED.status,
        output_data   = COALESCE(EXCLUDED.output_data, lenses.workflow_node_results.output_data),
        error_message = COALESCE(EXCLUDED.error_message, lenses.workflow_node_results.error_message),
        resolved_input_snapshot = COALESCE(
          EXCLUDED.resolved_input_snapshot,
          lenses.workflow_node_results.resolved_input_snapshot
        ),
        provider_route = COALESCE(EXCLUDED.provider_route, lenses.workflow_node_results.provider_route),
        started_at    = COALESCE(lenses.workflow_node_results.started_at, EXCLUDED.started_at),
        completed_at  = CASE
          WHEN EXCLUDED.status = ANY(v_terminal) THEN now()
          ELSE lenses.workflow_node_results.completed_at
        END;
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") IS 'Worker-only: upsert workflow_node_results with full jsonb output_data and optional resolved_input_snapshot / provider_route.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_child_run_id UUID;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM lenses.workflow_runs WHERE id = p_parent_run_id) THEN
    RAISE EXCEPTION 'Parent run not found' USING ERRCODE = 'P0002';
  END IF;

  INSERT INTO lenses.workflow_runs
    (workflow_id, triggered_by, status, context_inputs, trigger_mode)
  VALUES
    (p_child_workflow_id, NULL, 'pending', p_input, 'schedule')
  RETURNING id INTO v_child_run_id;

  INSERT INTO lenses.workflow_run_chains
    (parent_run_id, child_run_id, chain_reason)
  VALUES
    (p_parent_run_id, v_child_run_id, 'chained_run');

  RETURN v_child_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") IS 'CD: Creates a child run and records the parent→child chain relationship.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  rec       RECORD;
  dispatched INT := 0;
BEGIN
  FOR rec IN
    SELECT wt.id AS trigger_id, wt.workflow_id, wt.owner_id
      FROM lenses.workflow_triggers wt
     WHERE wt.enabled = TRUE
       AND wt.trigger_type = p_event_type
       AND public.fn_workflows_evaluate_condition(wt.condition, p_event_payload)
  LOOP
    INSERT INTO lenses.workflow_runs
      (workflow_id, triggered_by, status, context_inputs, trigger_mode)
    VALUES
      (rec.workflow_id, NULL, 'pending', p_event_payload, 'schedule');

    -- Update last_fired_at
    UPDATE lenses.workflow_triggers
       SET last_fired_at = now()
     WHERE id = rec.trigger_id;

    dispatched := dispatched + 1;
  END LOOP;

  RETURN dispatched;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") IS 'CD: Fan-out to enabled triggers matching event_type + condition. Returns dispatched count.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") RETURNS boolean
    LANGUAGE "plpgsql" IMMUTABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  k TEXT;
  v JSONB;
BEGIN
  -- Empty condition always matches
  IF p_condition = '{}'::JSONB OR p_condition IS NULL THEN
    RETURN TRUE;
  END IF;

  -- Each key in p_condition must match the corresponding key in p_event
  FOR k, v IN SELECT * FROM jsonb_each(p_condition)
  LOOP
    IF p_event -> k IS DISTINCT FROM v THEN
      RETURN FALSE;
    END IF;
  END LOOP;

  RETURN TRUE;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") IS 'CD: IMMUTABLE shallow key-match condition evaluator. Used by trigger fan-out.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 12, "p_search" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "node_count" bigint, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "hot_score" double precision)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility::text,
    w.battle_count,
    (
      SELECT count(*)
      FROM lenses.workflow_nodes wn
      WHERE wn.workflow_id = w.id
    ) AS node_count,
    w.reaction_totals,
    w.fork_count,
    w.parent_workflow_id,
    w.created_at,
    w.updated_at,
    log(
      GREATEST(
        1,
        5.0 * w.battle_count
        + 3.0 * w.fork_count
        + 4.0 * COALESCE((w.reaction_totals->>'copy')::int, 0)
        + 2.0 * COALESCE((w.reaction_totals->>'like')::int, 0)
        + 1.0 * COALESCE((w.reaction_totals->>'saved')::int, 0)
      )
    ) / NULLIF(
      pow((extract(epoch FROM now() - w.created_at) / 3600.0 + 2), 1.5),
      0
    ) AS hot_score
  FROM lenses.workflows w
  WHERE w.visibility::text = 'public'
    AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%')
  ORDER BY hot_score DESC, w.created_at DESC
  LIMIT p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_stored_secret TEXT;
  v_run_id        UUID;
BEGIN
  -- Find the webhook trigger for this workflow
  SELECT webhook_secret INTO v_stored_secret
    FROM lenses.workflow_triggers
   WHERE workflow_id   = p_workflow_id
     AND trigger_type  = 'webhook'
     AND enabled       = TRUE
   LIMIT 1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'No active webhook trigger for workflow' USING ERRCODE = 'P0002';
  END IF;

  IF v_stored_secret IS DISTINCT FROM p_secret THEN
    RAISE EXCEPTION 'invalid_webhook_secret' USING ERRCODE = 'P0001';
  END IF;

  INSERT INTO lenses.workflow_runs
    (workflow_id, triggered_by, status, context_inputs, trigger_mode)
  VALUES
    (p_workflow_id, NULL, 'pending', p_payload, 'schedule')
  RETURNING id INTO v_run_id;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") IS 'CD: HMAC-validated webhook dispatch. Raises P0001 invalid_webhook_secret on mismatch.';



CREATE OR REPLACE FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric DEFAULT 0.5, "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_team_run_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_profile agents.memory_profiles;
  v_id      uuid;
BEGIN
  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.memories (
    profile_id, ai_lenser_id, scope, source, content,
    embedding_metadata, confidence, expires_at, team_run_id
  )
  VALUES (
    p_profile_id, v_profile.ai_lenser_id, p_scope, p_source, p_content,
    COALESCE(p_metadata, '{}'::jsonb),
    COALESCE(p_confidence, 0.5),
    p_expires_at,
    p_team_run_id
  )
  RETURNING id INTO v_id;

  INSERT INTO agents.memory_access_logs (memory_id, team_run_id, action, context)
  VALUES (v_id, p_team_run_id, 'write', jsonb_build_object('source', p_source, 'scope', p_scope));

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  -- Restrict to service_role / supabase_admin
  IF current_setting('role', true) NOT IN ('service_role', 'supabase_admin') THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'fn_xp_freeze_content is restricted to service_role.',
            HINT    = 'permission_denied';
  END IF;

  -- Mark XP events as frozen in their meta JSONB without deleting the audit trail
  UPDATE xp.events
  SET meta = COALESCE(meta, '{}'::jsonb) || '{"frozen": true, "freeze_reason": "moderated_content"}'::jsonb
  WHERE source_ref_type = p_source_ref_type
    AND source_ref_id   = p_source_ref_id
    AND NOT COALESCE((meta->>'frozen')::boolean, false);

  GET DIAGNOSTICS v_count = ROW_COUNT;

  RAISE NOTICE 'fn_xp_freeze_content: froze % events for %/%', v_count, p_source_ref_type, p_source_ref_id;
  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") IS 'CB: Marks XP events for moderated/deleted content as frozen. Preserves audit trail; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_apps"() RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "difficulty" "text", "is_active" boolean)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp'
    AS $$
BEGIN
  RETURN QUERY
  SELECT a.id, a.slug::text, a.name::text, a.difficulty::text, a.is_active
  FROM xp.apps a
  WHERE a.is_active = true
  ORDER BY a.slug;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_apps"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_apps"() IS 'Returns all active XP app definitions.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "context" "text", "contribution_type" "text", "external_ref" "text", "title" "text", "verified_by" "uuid", "xp_event_id" "uuid", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_lenser_id IS NULL THEN
    SELECT id INTO v_lenser_id FROM lensers.profiles WHERE user_id = auth.uid();
  ELSE
    v_lenser_id := p_lenser_id;
  END IF;

  RETURN QUERY
  SELECT
    c.id, c.lenser_id, c.context::text, c.contribution_type::text,
    c.external_ref::text, c.title::text, c.verified_by, c.xp_event_id, c.created_at
  FROM xp.contributions c
  WHERE c.lenser_id = v_lenser_id
  ORDER BY c.created_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") IS 'Returns XP contributions for the given lenser or the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "action_key" "text", "xp" integer, "base_xp" integer, "source" "text", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_lenser_id IS NULL THEN
    SELECT id INTO v_lenser_id FROM lensers.profiles WHERE user_id = auth.uid();
  ELSE
    v_lenser_id := p_lenser_id;
  END IF;

  RETURN QUERY
  SELECT e.id, e.action_key::text, e.xp, e.base_xp, e.source::text, e.created_at
  FROM xp.events e
  WHERE e.lenser_id = v_lenser_id
  ORDER BY e.created_at DESC
  LIMIT LEAST(p_limit, 200);
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) IS 'Returns paginated XP event history for the given lenser or the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "old_level" integer, "new_level" integer, "total_xp_at" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  IF auth.uid() IS DISTINCT FROM p_lenser_id
     AND current_setting('role', true) NOT IN ('service_role', 'supabase_admin')
  THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'You may only read your own level-up history.',
            HINT    = 'permission_denied';
  END IF;

  RETURN QUERY
  SELECT
    lu.id,
    lu.old_level,
    lu.new_level,
    lu.total_xp_at,
    lu.created_at
  FROM xp.level_ups lu
  WHERE lu.lenser_id = p_lenser_id
  ORDER BY lu.created_at DESC
  LIMIT LEAST(p_limit, 100);
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) IS 'CB: Returns level-up history for a lenser. SECURITY DEFINER — callers may only read own history.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000001'::"uuid") RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "description" "text", "reward_description" "text", "featured_challenges" "jsonb", "starts_at" timestamp with time zone, "ends_at" timestamp with time zone, "is_active" boolean, "status" "text")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    s.id,
    s.slug,
    s.name,
    s.description,
    s.reward_description,
    s.featured_challenges,
    s.starts_at,
    s.ends_at,
    s.is_active,
    CASE
      WHEN s.is_active                    THEN 'active'
      WHEN s.starts_at > now()            THEN 'upcoming'
      ELSE                                     'ended'
    END AS status
  FROM xp.seasons s
  WHERE s.app_id = p_app_id
  ORDER BY s.starts_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") IS 'CB: Returns all seasons for an app with computed status (active/upcoming/ended).';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_self"() RETURNS TABLE("lenser_id" "uuid", "total_xp" bigint, "current_level" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'xp', 'auth'
    AS $$
begin
    return query
    select
        t.lenser_id,
        t.total_xp,
        t.current_level
    from xp.totals t
    join lensers.profiles p on p.id = t.lenser_id
    where p.user_id = auth.uid();
end;
$$;


ALTER FUNCTION "public"."fn_xp_get_self"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text" DEFAULT 'daily'::"text") RETURNS TABLE("lenser_id" "uuid", "streak_type" "text", "current_streak" integer, "best_streak" integer, "last_update_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  -- RLS: callers may only read own streak; service_role reads any
  IF auth.uid() IS DISTINCT FROM p_lenser_id
     AND current_setting('role', true) NOT IN ('service_role', 'supabase_admin')
  THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'You may only read your own streak.',
            HINT    = 'permission_denied';
  END IF;

  RETURN QUERY
  SELECT
    s.lenser_id,
    s.streak_type,
    s.current_streak,
    s.best_streak,
    s.last_update_at
  FROM xp.streaks s
  WHERE s.lenser_id    = p_lenser_id
    AND s.streak_type  = p_streak_type;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") IS 'CB: Returns streak info for a lenser. SECURITY DEFINER — callers may only read own streak.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid" DEFAULT NULL::"uuid", "p_app_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("total_xp" bigint, "current_level" integer, "app_id" "uuid", "min_total_xp" bigint, "max_total_xp" bigint)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_lenser_id IS NULL THEN
    SELECT id INTO v_lenser_id FROM lensers.profiles WHERE user_id = auth.uid();
  ELSE
    v_lenser_id := p_lenser_id;
  END IF;

  RETURN QUERY
  SELECT
    COALESCE(xrow.row_total_xp, 0)::bigint,
    COALESCE(xrow.row_level, 1),
    COALESCE(xrow.row_app_id, '00000000-0000-0000-0000-000000000001'::uuid),
    COALESCE(l.min_total_xp, 0)::bigint,
    l.max_total_xp::bigint
  FROM (
    SELECT xt.total_xp AS row_total_xp,
           xt.current_level AS row_level,
           xt.app_id AS row_app_id
    FROM xp.totals xt
    WHERE xt.lenser_id = v_lenser_id
      AND (p_app_id IS NULL OR xt.app_id = p_app_id)
    LIMIT 1
  ) xrow
  LEFT JOIN xp.levels l
    ON l.app_id = COALESCE(xrow.row_app_id, '00000000-0000-0000-0000-000000000001'::uuid)
   AND l.level  = COALESCE(xrow.row_level, 1);
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") IS 'Returns XP summary (total, level, level band) for the given lenser or the caller.';



CREATE OR REPLACE FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") RETURNS TABLE("id" "uuid", "name" "text", "key" "text", "provider_key" "text", "provider_name" "text", "context_window_tokens" integer, "supports_tools" boolean, "supports_json_schema" boolean, "supports_vision" boolean, "capabilities" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
    SELECT
        m.id,
        m.name,
        m.key,
        p.key               AS provider_key,
        p.display_name      AS provider_name,
        m.context_window_tokens,
        m.supports_tools,
        m.supports_json_schema,
        m.supports_vision,
        m.capabilities,
        m.created_at
    FROM  ai.models     m
    JOIN  ai.providers  p ON p.id = m.provider_id
    WHERE p.key       = p_provider_key
      AND p.is_active = true
      AND m.is_active = true
    ORDER BY m.name;
$$;


ALTER FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") IS 'Returns active models for a given active provider key. Column model_key renamed to key in migration 000059. SECURITY DEFINER — runs as postgres, bypasses RLS.';



CREATE OR REPLACE FUNCTION "public"."get_active_providers"() RETURNS TABLE("id" "uuid", "key" "text", "display_name" "text", "base_url" "text", "docs_url" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
    SELECT
        p.id,
        p.key,
        p.display_name,
        p.base_url,
        p.docs_url,
        p.metadata,
        p.created_at,
        p.updated_at
    FROM ai.providers p
    WHERE p.is_active = true
      AND EXISTS (
          SELECT 1
          FROM ai.models m
          WHERE m.provider_id = p.id
            AND m.is_active   = true
      )
    ORDER BY p.display_name;
$$;


ALTER FUNCTION "public"."get_active_providers"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."get_active_providers"() IS 'Returns active AI providers that have at least one active model. Providers that are marked active but have no active models are excluded. SECURITY DEFINER — runs as postgres. Callable by anon and authenticated. ai.providers is not directly accessible by those roles.';



CREATE OR REPLACE FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") RETURNS TABLE("model_id" "uuid", "unit_type" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
    SELECT m.id AS model_id, mp.unit_type::text
    FROM ai.models m
    JOIN ai.providers p ON p.id = m.provider_id
    JOIN ai.model_pricing mp
        ON mp.model_id = m.id
       AND mp.effective_to IS NULL
    WHERE p.key = p_provider_key
      AND m.key = p_model_key
      AND m.is_active = true
    LIMIT 1;
$$;


ALTER FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."guard_notification_fields"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO ''
    AS $$
BEGIN
  -- Service-role and internal functions may update any column
  IF current_user IN ('postgres', 'supabase_admin', 'service_role') THEN
    RETURN NEW;
  END IF;

  -- Silently reset protected columns to their original values.
  -- Only read_at is allowed to change for authenticated users.
  NEW.id         := OLD.id;
  NEW.lenser_id  := OLD.lenser_id;
  NEW.type       := OLD.type;
  NEW.title      := OLD.title;
  NEW.body       := OLD.body;
  NEW.action_url := OLD.action_url;
  NEW.metadata   := OLD.metadata;
  NEW.created_at := OLD.created_at;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."guard_notification_fields"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."is_admin"() RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  select coalesce((auth.jwt() ->> 'role') = 'admin', false);
$$;


ALTER FUNCTION "public"."is_admin"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."list_tags_stats"("p_lang" "text" DEFAULT 'en'::"text", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "language_code" "text", "created_at" timestamp with time zone, "created_count" bigint, "viewed_count" bigint, "reacted_count" bigint, "total_usage" bigint, "trend_score" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$
WITH daily AS (
    SELECT tag_id,
           sum(created_count) AS created_total,
           sum(viewed_count)  AS viewed_total,
           sum(reacted_count) AS reacted_total
    FROM analytics.tag_activity_daily
    GROUP BY tag_id
), recent_7d AS (
    SELECT tag_id,
           sum(
               (created_count * 1) +
               (viewed_count  * 2) +
               (reacted_count * 3)
           ) AS trend_score_7d
    FROM analytics.tag_activity_daily
    WHERE activity_date >= CURRENT_DATE - INTERVAL '7 days'
    GROUP BY tag_id
)
SELECT
    t.id,
    t.slug,
    COALESCE(tn_lang.name, tn_any.name, t.slug) AS name,
    COALESCE(tn_lang.lang, tn_any.lang, 'slug')  AS language_code,
    t.created_at,
    COALESCE(d.created_total,  0) AS created_count,
    COALESCE(d.viewed_total,   0) AS viewed_count,
    COALESCE(d.reacted_total,  0) AS reacted_count,
    (COALESCE(d.created_total, 0) + COALESCE(d.viewed_total, 0) + COALESCE(d.reacted_total, 0)) AS total_usage,
    COALESCE(r.trend_score_7d, 0) AS trend_score
FROM content.tags t
LEFT JOIN LATERAL (
    SELECT name, language_code AS lang
    FROM content.tag_translations
    WHERE tag_id       = t.id
      AND language_code = p_lang
    LIMIT 1
) tn_lang ON true
LEFT JOIN LATERAL (
    SELECT name, language_code AS lang
    FROM content.tag_translations
    WHERE tag_id = t.id
    LIMIT 1
) tn_any ON true
LEFT JOIN daily    d ON d.tag_id = t.id
LEFT JOIN recent_7d r ON r.tag_id = t.id
WHERE t.visibility = 'public'::content.tag_visibility_enum
ORDER BY trend_score_7d DESC NULLS LAST, total_usage DESC
LIMIT  LEAST(p_limit, 100)
OFFSET p_offset;
$$;


ALTER FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) IS 'Returns top N public tags ordered by 7-day trend score.
Language resolution: tries p_lang first, falls back to any available translation, then slug.
Row cap: min(p_limit, 100). Default: top 50 tags in "en".';



CREATE OR REPLACE FUNCTION "public"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
    NEW.updated_at := now();
    RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."set_updated_at"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."set_updated_at"() IS 'Shared BEFORE UPDATE trigger function. Sets updated_at = now(). SECURITY DEFINER, search_path locked to empty. Used cross-schema by wallet, billing, and content updated_at triggers.';



CREATE OR REPLACE FUNCTION "public"."trg_block_unsafe_artifact_delete"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  v_type TEXT;
  v_summary JSONB;
BEGIN
  IF public.fn_lifecycle_is_service_role() THEN
    RETURN OLD;
  END IF;

  v_type := CASE TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME
    WHEN 'lenses.lenses' THEN 'lens'
    WHEN 'lenses.workflows' THEN 'workflow'
    WHEN 'battles.battles' THEN 'battle'
    WHEN 'agents.ai_lensers' THEN 'agent'
    ELSE NULL
  END;

  IF v_type IS NULL THEN
    RETURN OLD;
  END IF;

  v_summary := public.fn_artifact_dependency_summary(v_type, OLD.id);

  IF COALESCE((v_summary ->> 'total')::int, 0) > 0 THEN
    RAISE EXCEPTION 'unsafe_hard_delete_blocked: use lifecycle delete/archive RPC instead'
      USING ERRCODE = '55000',
            DETAIL = v_summary::text;
  END IF;

  RETURN OLD;
END;
$$;


ALTER FUNCTION "public"."trg_block_unsafe_artifact_delete"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "reputation"."fn_seed_vote_risk"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'reputation', 'net', 'public'
    AS $$
DECLARE
  v_url             text;
  v_service_key     text;
BEGIN
  -- ── Step 1: Synchronous zero-risk seed ─────────────────────────────────────
  -- Always runs. Ensures the FK row exists for immediate joins in aggregations.
  INSERT INTO reputation.vote_risk_scores (vote_id, risk_score, risk_factors)
  VALUES (NEW.id, 0, '{}')
  ON CONFLICT (vote_id) DO NOTHING;

  -- ── Step 2: Async risk enrichment via pg_net ────────────────────────────────
  -- Only fires when app.supabase_url is set (i.e. hosted Supabase environment).
  -- In local dev without this GUC configured the seed-only path is sufficient.
  v_url         := current_setting('app.supabase_url',      true);
  v_service_key := internal.get_service_role_key();

  IF v_url IS NOT NULL AND v_url <> '' AND v_service_key IS NOT NULL THEN
    PERFORM net.http_post(
      url     := v_url || '/functions/v1/score-vote-risk',
      body    := jsonb_build_object(
                   'vote_id',    NEW.id,
                   'battle_id',  NEW.battle_id,
                   'voter_id',   NEW.voter_lenser_id,
                   'voted_for',  NEW.voted_contender_id,
                   'vote_value', NEW.vote_value::text,
                   'is_draw',    NEW.is_draw
                 ),
      headers := jsonb_build_object(
                   'Content-Type',  'application/json',
                   'Authorization', 'Bearer ' || v_service_key
                 )
    );
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "reputation"."fn_seed_vote_risk"() OWNER TO "postgres";


COMMENT ON FUNCTION "reputation"."fn_seed_vote_risk"() IS 'AFTER INSERT trigger on battles.votes. Phase F: seeds zero-risk row synchronously then enqueues async HTTP POST to score-vote-risk Edge Function via pg_net when app.supabase_url is configured. Gracefully degrades to seed-only in local dev when GUC is unset. GRASP Protected Variations: DB trigger is decoupled from risk model logic.';



CREATE OR REPLACE FUNCTION "reputation"."fn_trueskill_update"("p_mu1" numeric, "p_sigma1" numeric, "p_mu2" numeric, "p_sigma2" numeric, "p_outcome" numeric, "p_tau" numeric DEFAULT 0.0833, "p_beta" numeric DEFAULT 4.1667) RETURNS TABLE("new_mu1" numeric, "new_sigma1" numeric, "new_mu2" numeric, "new_sigma2" numeric)
    LANGUAGE "plpgsql" IMMUTABLE SECURITY DEFINER
    SET "search_path" TO 'reputation', 'public'
    AS $$
DECLARE
  v_sigma1_sq numeric;
  v_sigma2_sq numeric;
  v_c         numeric;  -- Total performance uncertainty
  v_t         numeric;  -- Standardized difference
  v_v         numeric;  -- Mean additive correction (truncated Gaussian)
  v_w         numeric;  -- Variance multiplicative correction
  v_epsilon   numeric := 0.0;  -- Draw margin (0 for no-draw model)
  v_pdf_val   numeric;
  v_cdf_val   numeric;
  v_sign      numeric;
BEGIN
  -- Apply dynamics (tau) to account for skill drift
  v_sigma1_sq := p_sigma1 * p_sigma1 + p_tau * p_tau;
  v_sigma2_sq := p_sigma2 * p_sigma2 + p_tau * p_tau;

  -- Total uncertainty: c = sqrt(2*beta^2 + sigma1^2 + sigma2^2)
  v_c := sqrt(2.0 * p_beta * p_beta + v_sigma1_sq + v_sigma2_sq);

  IF p_outcome >= 0.99 THEN
    -- Player 1 wins
    v_t := (p_mu1 - p_mu2) / v_c;

    v_pdf_val := reputation.std_normal_pdf(v_t);
    v_cdf_val := reputation.std_normal_cdf(v_t);

    -- Guard against division by zero
    IF v_cdf_val < 0.000001 THEN v_cdf_val := 0.000001; END IF;

    v_v := v_pdf_val / v_cdf_val;
    v_w := v_v * (v_v + v_t);

    RETURN QUERY SELECT
      (p_mu1 + (v_sigma1_sq / v_c) * v_v)::numeric,
      (sqrt(v_sigma1_sq * (1.0 - (v_sigma1_sq / (v_c * v_c)) * v_w)))::numeric,
      (p_mu2 - (v_sigma2_sq / v_c) * v_v)::numeric,
      (sqrt(v_sigma2_sq * (1.0 - (v_sigma2_sq / (v_c * v_c)) * v_w)))::numeric;

  ELSIF p_outcome <= 0.01 THEN
    -- Player 2 wins: swap perspective
    v_t := (p_mu2 - p_mu1) / v_c;

    v_pdf_val := reputation.std_normal_pdf(v_t);
    v_cdf_val := reputation.std_normal_cdf(v_t);

    IF v_cdf_val < 0.000001 THEN v_cdf_val := 0.000001; END IF;

    v_v := v_pdf_val / v_cdf_val;
    v_w := v_v * (v_v + v_t);

    RETURN QUERY SELECT
      (p_mu1 - (v_sigma1_sq / v_c) * v_v)::numeric,
      (sqrt(v_sigma1_sq * (1.0 - (v_sigma1_sq / (v_c * v_c)) * v_w)))::numeric,
      (p_mu2 + (v_sigma2_sq / v_c) * v_v)::numeric,
      (sqrt(v_sigma2_sq * (1.0 - (v_sigma2_sq / (v_c * v_c)) * v_w)))::numeric;

  ELSE
    -- Draw: use draw factor (v and w for draws differ)
    v_t := abs(p_mu1 - p_mu2) / v_c;

    v_pdf_val := reputation.std_normal_pdf(v_t);
    v_cdf_val := reputation.std_normal_cdf(v_t);

    -- For draws, both players move toward each other
    IF v_cdf_val < 0.000001 THEN v_cdf_val := 0.000001; END IF;

    v_v := v_pdf_val / v_cdf_val;
    v_w := v_v * (v_v + v_t);

    -- Symmetric update: both move toward the mean
    v_sign := CASE WHEN p_mu1 > p_mu2 THEN -1.0 ELSE 1.0 END;

    RETURN QUERY SELECT
      (p_mu1 + v_sign * (v_sigma1_sq / v_c) * v_v * 0.5)::numeric,
      (sqrt(v_sigma1_sq * (1.0 - (v_sigma1_sq / (v_c * v_c)) * v_w * 0.5)))::numeric,
      (p_mu2 - v_sign * (v_sigma2_sq / v_c) * v_v * 0.5)::numeric,
      (sqrt(v_sigma2_sq * (1.0 - (v_sigma2_sq / (v_c * v_c)) * v_w * 0.5)))::numeric;
  END IF;
END;
$$;


ALTER FUNCTION "reputation"."fn_trueskill_update"("p_mu1" numeric, "p_sigma1" numeric, "p_mu2" numeric, "p_sigma2" numeric, "p_outcome" numeric, "p_tau" numeric, "p_beta" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "reputation"."fn_trueskill_update"("p_mu1" numeric, "p_sigma1" numeric, "p_mu2" numeric, "p_sigma2" numeric, "p_outcome" numeric, "p_tau" numeric, "p_beta" numeric) IS 'TrueSkill factor graph update for 1v1 matches. Returns updated (mu, sigma) for both players. Uses tau for dynamics and beta for performance variance.';



CREATE OR REPLACE FUNCTION "reputation"."fn_update_elo_on_finalize"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'reputation', 'public'
    AS $$
DECLARE
  v_contender_a     RECORD;
  v_contender_b     RECORD;
  v_score_a         numeric;
  v_score_b         numeric;
  v_lenser_a        uuid;
  v_lenser_b        uuid;
  v_rating_a        RECORD;
  v_rating_b        RECORD;
  v_new_a           RECORD;
  v_new_b           RECORD;
  v_category        text;
  v_use_trueskill   boolean;
BEGIN
  -- Only fire on status transition → 'closed'
  IF NEW.status <> 'closed' OR OLD.status = 'closed' THEN
    RETURN NEW;
  END IF;

  -- ── Resolve contenders via entity map (1v1 assumption) ──────────────────
  SELECT em.contender_id, COALESCE(em.profile_id, em.ai_lenser_id) AS lenser_id
  INTO v_contender_a
  FROM battles.contenders c
  JOIN battles.contender_entity_map em ON em.contender_id = c.id
  WHERE c.battle_id = NEW.id AND c.contender_status = 'active'
    AND COALESCE(em.profile_id, em.ai_lenser_id) IS NOT NULL
  ORDER BY c.created_at ASC
  LIMIT 1;

  SELECT em.contender_id, COALESCE(em.profile_id, em.ai_lenser_id) AS lenser_id
  INTO v_contender_b
  FROM battles.contenders c
  JOIN battles.contender_entity_map em ON em.contender_id = c.id
  WHERE c.battle_id = NEW.id AND c.contender_status = 'active'
    AND COALESCE(em.profile_id, em.ai_lenser_id) IS NOT NULL
    AND c.id <> v_contender_a.contender_id
  ORDER BY c.created_at ASC
  LIMIT 1;

  IF v_contender_a.lenser_id IS NULL OR v_contender_b.lenser_id IS NULL THEN
    RETURN NEW;
  END IF;

  v_lenser_a := v_contender_a.lenser_id;
  v_lenser_b := v_contender_b.lenser_id;

  -- ── Determine outcomes ───────────────────────────────────────────────────
  IF NEW.winner_contender_id IS NULL THEN
    v_score_a := 0.5;
    v_score_b := 0.5;
  ELSIF NEW.winner_contender_id = v_contender_a.contender_id THEN
    v_score_a := 1.0;
    v_score_b := 0.0;
  ELSE
    v_score_a := 0.0;
    v_score_b := 1.0;
  END IF;

  -- ── Derive category from battle_type (fallback to 'general') ────────────
  v_category := coalesce(NEW.content_type, NEW.battle_type::text, 'general');

  -- ── Upsert ratings for both players ─────────────────────────────────────
  INSERT INTO reputation.contender_ratings (lenser_id, category)
  VALUES (v_lenser_a, v_category)
  ON CONFLICT (lenser_id, category) DO NOTHING;

  INSERT INTO reputation.contender_ratings (lenser_id, category)
  VALUES (v_lenser_b, v_category)
  ON CONFLICT (lenser_id, category) DO NOTHING;

  -- Load current ratings
  SELECT * INTO v_rating_a
  FROM reputation.contender_ratings
  WHERE lenser_id = v_lenser_a AND category = v_category;

  SELECT * INTO v_rating_b
  FROM reputation.contender_ratings
  WHERE lenser_id = v_lenser_b AND category = v_category;

  -- ── Decide: TrueSkill (tau > 0) or Glicko-2 ────────────────────────────
  v_use_trueskill := (v_rating_a.tau > 0 OR v_rating_b.tau > 0);

  IF v_use_trueskill THEN
    -- TrueSkill update
    SELECT * INTO v_new_a
    FROM reputation.fn_trueskill_update(
      v_rating_a.elo_rating, v_rating_a.uncertainty,
      v_rating_b.elo_rating, v_rating_b.uncertainty,
      v_score_a,
      greatest(v_rating_a.tau, v_rating_b.tau),
      greatest(v_rating_a.beta, v_rating_b.beta)
    );

    -- Update player A
    UPDATE reputation.contender_ratings SET
      elo_rating     = v_new_a.new_mu1,
      uncertainty    = v_new_a.new_sigma1,
      battles_played = battles_played + 1,
      wins           = wins + CASE WHEN v_score_a >= 0.99 THEN 1 ELSE 0 END,
      draws          = draws + CASE WHEN v_score_a > 0.01 AND v_score_a < 0.99 THEN 1 ELSE 0 END,
      losses         = losses + CASE WHEN v_score_a <= 0.01 THEN 1 ELSE 0 END,
      updated_at     = now()
    WHERE lenser_id = v_lenser_a AND category = v_category;

    -- Update player B
    UPDATE reputation.contender_ratings SET
      elo_rating     = v_new_a.new_mu2,
      uncertainty    = v_new_a.new_sigma2,
      battles_played = battles_played + 1,
      wins           = wins + CASE WHEN v_score_b >= 0.99 THEN 1 ELSE 0 END,
      draws          = draws + CASE WHEN v_score_b > 0.01 AND v_score_b < 0.99 THEN 1 ELSE 0 END,
      losses         = losses + CASE WHEN v_score_b <= 0.01 THEN 1 ELSE 0 END,
      updated_at     = now()
    WHERE lenser_id = v_lenser_b AND category = v_category;

  ELSE
    -- Glicko-2 update (existing behavior)
    SELECT * INTO v_new_a
    FROM reputation.glicko2_update(
      v_rating_a.elo_rating, v_rating_a.uncertainty, v_rating_a.sigma,
      v_rating_b.elo_rating, v_rating_b.uncertainty,
      v_score_a
    );

    SELECT * INTO v_new_b
    FROM reputation.glicko2_update(
      v_rating_b.elo_rating, v_rating_b.uncertainty, v_rating_b.sigma,
      v_rating_a.elo_rating, v_rating_a.uncertainty,
      v_score_b
    );

    UPDATE reputation.contender_ratings SET
      elo_rating     = v_new_a.new_r,
      uncertainty    = v_new_a.new_rd,
      sigma          = v_new_a.new_sigma,
      battles_played = battles_played + 1,
      wins           = wins + CASE WHEN v_score_a >= 0.99 THEN 1 ELSE 0 END,
      draws          = draws + CASE WHEN v_score_a > 0.01 AND v_score_a < 0.99 THEN 1 ELSE 0 END,
      losses         = losses + CASE WHEN v_score_a <= 0.01 THEN 1 ELSE 0 END,
      updated_at     = now()
    WHERE lenser_id = v_lenser_a AND category = v_category;

    UPDATE reputation.contender_ratings SET
      elo_rating     = v_new_b.new_r,
      uncertainty    = v_new_b.new_rd,
      sigma          = v_new_b.new_sigma,
      battles_played = battles_played + 1,
      wins           = wins + CASE WHEN v_score_b >= 0.99 THEN 1 ELSE 0 END,
      draws          = draws + CASE WHEN v_score_b > 0.01 AND v_score_b < 0.99 THEN 1 ELSE 0 END,
      losses         = losses + CASE WHEN v_score_b <= 0.01 THEN 1 ELSE 0 END,
      updated_at     = now()
    WHERE lenser_id = v_lenser_b AND category = v_category;
  END IF;

  -- ── Upsert lenser_scores with latest ELO ────────────────────────────────
  INSERT INTO reputation.lenser_scores (lenser_id, score_type, score, uncertainty, computed_at)
  VALUES (v_lenser_a, 'elo', v_rating_a.elo_rating, v_rating_a.uncertainty, now())
  ON CONFLICT (lenser_id, score_type) DO UPDATE SET
    score       = EXCLUDED.score,
    uncertainty = EXCLUDED.uncertainty,
    computed_at = now();

  INSERT INTO reputation.lenser_scores (lenser_id, score_type, score, uncertainty, computed_at)
  VALUES (v_lenser_b, 'elo', v_rating_b.elo_rating, v_rating_b.uncertainty, now())
  ON CONFLICT (lenser_id, score_type) DO UPDATE SET
    score       = EXCLUDED.score,
    uncertainty = EXCLUDED.uncertainty,
    computed_at = now();

  RETURN NEW;
END;
$$;


ALTER FUNCTION "reputation"."fn_update_elo_on_finalize"() OWNER TO "postgres";


COMMENT ON FUNCTION "reputation"."fn_update_elo_on_finalize"() IS 'AFTER UPDATE trigger on battles.battles. Fires on status → ''closed''. Supports both Glicko-2 and TrueSkill: uses TrueSkill when contender_ratings.tau > 0, otherwise Glicko-2. Updates elo_rating, uncertainty, sigma/tau for both contenders. Also upserts reputation.lenser_scores.';



CREATE OR REPLACE FUNCTION "reputation"."glicko2_update"("p_r" numeric, "p_rd" numeric, "p_sigma" numeric, "p_opp_r" numeric, "p_opp_rd" numeric, "p_score" numeric) RETURNS TABLE("new_r" numeric, "new_rd" numeric, "new_sigma" numeric)
    LANGUAGE "plpgsql" IMMUTABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  -- Glicko-2 constants
  v_tau    CONSTANT numeric := 0.5;       -- system volatility constant τ
  v_scale  CONSTANT numeric := 173.7178;  -- Glicko-2 → Elo conversion factor
  v_eps    CONSTANT numeric := 0.000001;  -- Illinois convergence tolerance

  -- Glicko-2 internal scale
  v_mu     numeric;
  v_phi    numeric;
  v_mu_j   numeric;
  v_phi_j  numeric;

  -- Intermediate computation
  v_g      numeric;   -- g(φ_j)
  v_E      numeric;   -- E(μ, μ_j, φ_j)
  v_v      numeric;   -- estimated variance
  v_delta  numeric;   -- improvement Δ

  -- Illinois algorithm variables
  v_a      numeric;
  v_alpha  numeric;
  v_B      numeric;
  v_fA     numeric;
  v_fB     numeric;
  v_C      numeric;
  v_fC     numeric;
  v_iter   integer;
  v_ex     numeric;
  v_denom  numeric;

  -- New values
  v_sigma2    numeric;
  v_phi_star  numeric;
  v_phi_new   numeric;
  v_mu_new    numeric;
BEGIN
  -- Guard: RD must be positive
  p_rd     := GREATEST(p_rd,     1.0);
  p_opp_rd := GREATEST(p_opp_rd, 1.0);
  p_sigma  := GREATEST(p_sigma,  0.001);

  -- Step 1: Convert to Glicko-2 internal scale
  v_mu    := (p_r     - 1500.0) / v_scale;
  v_phi   := p_rd     / v_scale;
  v_mu_j  := (p_opp_r - 1500.0) / v_scale;
  v_phi_j := p_opp_rd / v_scale;

  -- Step 2: Compute g(φ_j) and E(μ, μ_j, φ_j)
  v_g := 1.0 / sqrt(1.0 + 3.0 * v_phi_j^2 / pi()^2);
  v_E := 1.0 / (1.0 + exp(-v_g * (v_mu - v_mu_j)));

  -- Step 3: Estimated variance v = 1 / (g² · E · (1 - E))
  v_v := 1.0 / GREATEST(v_g^2 * v_E * (1.0 - v_E), 0.0001);

  -- Step 4: Improvement Δ = v · g · (s - E)
  v_delta := v_v * v_g * (p_score - v_E);

  -- Step 5: Illinois algorithm to find new volatility σ'
  v_a := ln(p_sigma^2);
  v_alpha := v_a;

  IF v_delta^2 > v_phi^2 + v_v THEN
    v_B := ln(v_delta^2 - v_phi^2 - v_v);
  ELSE
    v_B := v_a - v_tau;
    v_iter := 0;
    LOOP
      v_ex    := exp(v_B);
      v_denom := v_phi^2 + v_v + v_ex;
      v_fB    := v_ex * (v_delta^2 - v_phi^2 - v_v - v_ex) / (2.0 * v_denom^2)
                 - (v_B - v_a) / v_tau^2;
      EXIT WHEN v_fB < 0 OR v_iter >= 200;
      v_B    := v_B - v_tau;
      v_iter := v_iter + 1;
    END LOOP;
  END IF;

  -- Compute f(A) and f(B)
  v_ex   := exp(v_alpha);
  v_denom := v_phi^2 + v_v + v_ex;
  v_fA   := v_ex * (v_delta^2 - v_phi^2 - v_v - v_ex) / (2.0 * v_denom^2)
             - (v_alpha - v_a) / v_tau^2;

  v_ex   := exp(v_B);
  v_denom := v_phi^2 + v_v + v_ex;
  v_fB   := v_ex * (v_delta^2 - v_phi^2 - v_v - v_ex) / (2.0 * v_denom^2)
             - (v_B - v_a) / v_tau^2;

  -- Illinois iteration
  v_iter := 0;
  WHILE abs(v_B - v_alpha) > v_eps AND v_iter < 500 LOOP
    v_C    := v_alpha + (v_alpha - v_B) * v_fA / NULLIF(v_fB - v_fA, 0);
    v_C    := COALESCE(v_C, (v_alpha + v_B) / 2.0);

    v_ex   := exp(v_C);
    v_denom := v_phi^2 + v_v + v_ex;
    v_fC   := v_ex * (v_delta^2 - v_phi^2 - v_v - v_ex) / (2.0 * v_denom^2)
               - (v_C - v_a) / v_tau^2;

    IF v_fC * v_fB < 0 THEN
      v_alpha  := v_B;
      v_fA := v_fB;
    ELSE
      v_fA := v_fA / 2.0;
    END IF;
    v_B    := v_C;
    v_fB   := v_fC;
    v_iter := v_iter + 1;
  END LOOP;

  v_sigma2 := exp(v_alpha / 2.0);

  -- Step 6: Pre-rating period RD: φ* = sqrt(φ² + σ'²)
  v_phi_star := sqrt(v_phi^2 + v_sigma2^2);

  -- Step 7: New φ' = 1 / sqrt(1/φ*² + 1/v)
  v_phi_new := 1.0 / sqrt(1.0 / v_phi_star^2 + 1.0 / v_v);

  -- Step 8: New μ' = μ + φ'² · g · (s - E)
  v_mu_new := v_mu + v_phi_new^2 * v_g * (p_score - v_E);

  -- Convert back to external Elo scale and return
  RETURN QUERY SELECT
    ROUND(v_scale * v_mu_new + 1500.0, 4)::numeric,
    ROUND(GREATEST(v_scale * v_phi_new, 30.0), 4)::numeric,  -- floor RD at 30
    ROUND(LEAST(GREATEST(v_sigma2, 0.001), 0.1), 6)::numeric; -- clamp σ ∈ [0.001, 0.1]
END;
$$;


ALTER FUNCTION "reputation"."glicko2_update"("p_r" numeric, "p_rd" numeric, "p_sigma" numeric, "p_opp_r" numeric, "p_opp_rd" numeric, "p_score" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "reputation"."glicko2_update"("p_r" numeric, "p_rd" numeric, "p_sigma" numeric, "p_opp_r" numeric, "p_opp_rd" numeric, "p_score" numeric) IS 'Computes one Glicko-2 rating period update for a single player against a single opponent. Inputs/outputs are in external Elo scale (same units as contender_ratings.elo_rating and .uncertainty). Uses Illinois algorithm for volatility convergence. GRASP Information Expert: all ELO math is encapsulated here.';



CREATE OR REPLACE FUNCTION "reputation"."std_normal_cdf"("x" numeric) RETURNS numeric
    LANGUAGE "plpgsql" IMMUTABLE
    SET "search_path" TO 'reputation', 'pg_catalog'
    AS $$
DECLARE
  v_t   numeric;
  v_d   numeric;
  v_abs numeric;
BEGIN
  v_abs := abs(x);
  v_t := 1.0 / (1.0 + 0.2316419 * v_abs);
  v_d := 0.3989422804014327 * exp(-0.5 * v_abs * v_abs);
  v_d := v_d * v_t * (0.3193815 + v_t * (-0.3565638 + v_t * (1.781478 + v_t * (-1.821256 + v_t * 1.330274))));

  IF x >= 0 THEN
    RETURN 1.0 - v_d;
  ELSE
    RETURN v_d;
  END IF;
END;
$$;


ALTER FUNCTION "reputation"."std_normal_cdf"("x" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "reputation"."std_normal_pdf"("x" numeric) RETURNS numeric
    LANGUAGE "sql" IMMUTABLE
    SET "search_path" TO 'reputation', 'pg_catalog'
    AS $$
  SELECT (1.0 / sqrt(2.0 * pi())) * exp(-0.5 * x * x);
$$;


ALTER FUNCTION "reputation"."std_normal_pdf"("x" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "status"."fn_status_incident_detail"("p_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'status', 'public'
    AS $$
select
  to_jsonb(i) || jsonb_build_object(
    'updates',
    coalesce((
      select jsonb_agg(to_jsonb(u) order by u.created_at asc)
      from status.v_public_incident_updates u
      where u.incident_id = i.id
    ), '[]'::jsonb)
  )
from status.v_public_incidents i
where i.id = p_id
limit 1;
$$;


ALTER FUNCTION "status"."fn_status_incident_detail"("p_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "status"."fn_status_service_detail"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'status', 'public'
    AS $$
with service_row as (
  select *
  from status.v_public_services
  where slug = p_slug
  limit 1
)
select case
  when exists (select 1 from service_row) then (
    select jsonb_build_object(
      'service', to_jsonb(s),
      'components', coalesce((
        select jsonb_agg(to_jsonb(c) order by c.sort_order, c.name)
        from status.v_public_components c
        where c.service_id = s.id
      ), '[]'::jsonb),
      'incidents', coalesce((
        select jsonb_agg(to_jsonb(i) order by i.started_at desc, i.created_at desc)
        from status.v_public_incidents i
        where i.service_id = s.id
      ), '[]'::jsonb),
      'maintenance', coalesce((
        select jsonb_agg(to_jsonb(m) order by m.starts_at asc)
        from status.v_public_maintenance_windows m
        where m.service_id = s.id
      ), '[]'::jsonb),
      'uptime', coalesce((
        select jsonb_object_agg(r.rollup_window::text, r.uptime_percent order by r.rollup_window)
        from status.uptime_rollups r
        where r.scope_type = 'service'
          and r.scope_key = s.slug
      ), '{}'::jsonb)
    )
    from service_row s
  )
  else null
end;
$$;


ALTER FUNCTION "status"."fn_status_service_detail"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "status"."fn_status_snapshot"("p_snapshot_date" "date", "p_scope_type" "text" DEFAULT 'platform'::"text", "p_scope_key" "text" DEFAULT 'global'::"text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'status', 'public'
    AS $$
select to_jsonb(s)
from status.daily_snapshots s
where s.snapshot_date = p_snapshot_date
  and s.scope_type = p_scope_type
  and s.scope_key = p_scope_key
limit 1;
$$;


ALTER FUNCTION "status"."fn_status_snapshot"("p_snapshot_date" "date", "p_scope_type" "text", "p_scope_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "status"."fn_status_summary"() RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'status', 'public'
    AS $$
with public_services as (
  select *
  from status.v_public_services
  order by sort_order, name
),
active_incidents as (
  select *
  from status.v_public_incidents
  where status in ('investigating', 'identified', 'monitoring')
  order by started_at desc, created_at desc
),
active_maintenance as (
  select *
  from status.v_public_maintenance_windows
  where status in ('scheduled', 'in_progress')
  order by starts_at asc
),
platform_status as (
  select
    case
      when exists (
        select 1 from public_services where current_status = 'major_outage'
      ) then 'major_outage'::text
      when exists (
        select 1 from public_services where current_status = 'partial_outage'
      ) then 'partial_outage'::text
      when exists (
        select 1 from public_services where current_status = 'degraded_performance'
      ) then 'degraded_performance'::text
      when exists (
        select 1 from public_services where current_status = 'maintenance'
      ) then 'maintenance'::text
      else 'operational'::text
    end as overall_status
)
select jsonb_build_object(
  'headline',
    case
      when not exists (select 1 from public_services) then 'Public status surface is initializing'
      when (select overall_status from platform_status) = 'operational' then 'All systems operational'
      when (select overall_status from platform_status) = 'degraded_performance' then 'Some systems experiencing degraded performance'
      when (select overall_status from platform_status) = 'partial_outage' then 'Some systems experiencing a partial outage'
      when (select overall_status from platform_status) = 'major_outage' then 'Major outage affecting one or more systems'
      when (select overall_status from platform_status) = 'maintenance' then 'Scheduled maintenance in progress'
      else 'Status unavailable'
    end,
  'status', (select overall_status from platform_status),
  'indicator',
    case
      when (select overall_status from platform_status) = 'operational' then 'none'
      when (select overall_status from platform_status) = 'degraded_performance' then 'minor'
      when (select overall_status from platform_status) = 'partial_outage' then 'major'
      when (select overall_status from platform_status) = 'major_outage' then 'critical'
      when (select overall_status from platform_status) = 'maintenance' then 'maintenance'
      else 'none'
    end,
  'services', coalesce(
    (select jsonb_agg(to_jsonb(ps) order by ps.sort_order, ps.name) from public_services ps),
    '[]'::jsonb
  ),
  'incidents', coalesce(
    (select jsonb_agg(to_jsonb(ai) order by ai.started_at desc, ai.created_at desc) from active_incidents ai),
    '[]'::jsonb
  ),
  'maintenance', coalesce(
    (select jsonb_agg(to_jsonb(am) order by am.starts_at asc) from active_maintenance am),
    '[]'::jsonb
  ),
  'updated_at', coalesce((select max(updated_at) from public_services), now())
);
$$;


ALTER FUNCTION "status"."fn_status_summary"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "status"."fn_status_uptime_rollup"("p_window" "status"."status_window", "p_scope_type" "text" DEFAULT 'platform'::"text", "p_scope_key" "text" DEFAULT 'global'::"text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'status', 'public'
    AS $$
select to_jsonb(r)
from status.uptime_rollups r
where r.rollup_window = p_window
  and r.scope_type = p_scope_type
  and r.scope_key = p_scope_key
limit 1;
$$;


ALTER FUNCTION "status"."fn_status_uptime_rollup"("p_window" "status"."status_window", "p_scope_type" "text", "p_scope_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "status"."touch_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'status'
    AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$;


ALTER FUNCTION "status"."touch_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "tenancy"."fn_create_personal_workspace"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'tenancy', 'lensers', 'public'
    AS $$
DECLARE
    v_workspace_id uuid;
    v_slug         text;
BEGIN
    IF EXISTS (
        SELECT 1 FROM tenancy.workspaces
        WHERE owner_lenser_id = NEW.id AND type = 'personal'
    ) THEN
        RETURN NEW;
    END IF;

    -- Map profile handle → workspace slug (no dots; collapse runs of separators).
    v_slug := trim(both '_' FROM regexp_replace(
        regexp_replace(lower(NEW.handle), '[^a-z0-9]+', '_', 'g'),
        '_+', '_', 'g'
    ));

    IF v_slug IS NULL OR length(v_slug) < 3 THEN
        v_slug := 'lwf_' || replace(NEW.id::text, '-', '');
    END IF;

    v_slug := left(v_slug, 64);

    BEGIN
        INSERT INTO tenancy.workspaces (slug, type, display_name, owner_lenser_id)
        VALUES (v_slug, 'personal', COALESCE(NEW.display_name, NEW.handle), NEW.id)
        RETURNING id INTO v_workspace_id;

    EXCEPTION WHEN unique_violation THEN
        v_slug := left(
            regexp_replace(
                v_slug || '_' || replace(NEW.id::text, '-', ''),
                '_+', '_', 'g'
            ),
            64
        );

        INSERT INTO tenancy.workspaces (slug, type, display_name, owner_lenser_id)
        VALUES (v_slug, 'personal', COALESCE(NEW.display_name, NEW.handle), NEW.id)
        RETURNING id INTO v_workspace_id;
    END;

    INSERT INTO tenancy.workspace_members (workspace_id, lenser_id, role)
    VALUES (v_workspace_id, NEW.id, 'owner')
    ON CONFLICT (workspace_id, lenser_id) DO NOTHING;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "tenancy"."fn_create_personal_workspace"() OWNER TO "postgres";


COMMENT ON FUNCTION "tenancy"."fn_create_personal_workspace"() IS 'AFTER INSERT OR UPDATE on lensers.profiles. Creates a personal workspace slug derived from handle (workspace-safe: dots and repeated separators normalized). Idempotent.';



CREATE OR REPLACE FUNCTION "tenancy"."fn_guard_personal_workspace"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'tenancy', 'lensers', 'public'
    AS $$
BEGIN
    -- Forbid directly deleting a personal workspace while the owning profile exists
    IF OLD.type = 'personal' AND EXISTS (
        SELECT 1 FROM lensers.profiles WHERE id = OLD.owner_lenser_id
    ) THEN
        RAISE EXCEPTION
            'Cannot delete personal workspace % while lenser profile % still exists. '
            'Delete the lensers.profiles row instead — the workspace will cascade.',
            OLD.id, OLD.owner_lenser_id
            USING ERRCODE = 'restrict_violation';
    END IF;

    RETURN OLD;
END;
$$;


ALTER FUNCTION "tenancy"."fn_guard_personal_workspace"() OWNER TO "postgres";


COMMENT ON FUNCTION "tenancy"."fn_guard_personal_workspace"() IS 'BEFORE DELETE guard on tenancy.workspaces. Blocks direct deletion of a personal workspace while the owning lenser profile still exists. Cascade deletes via lensers.profiles are still allowed because the profile row is gone by then.';



CREATE OR REPLACE FUNCTION "tenancy"."fn_guard_personal_workspace_status"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'tenancy', 'lensers', 'public'
    AS $$
BEGIN
    IF OLD.type = 'personal'
       AND NEW.status IS DISTINCT FROM 'active'
       AND OLD.status = 'active'
       AND EXISTS (
           SELECT 1 FROM lensers.profiles WHERE id = OLD.owner_lenser_id
       )
    THEN
        RAISE EXCEPTION
            'Cannot deactivate personal workspace % while lenser profile % still exists. '
            'Update lensers.profiles.status instead.',
            OLD.id, OLD.owner_lenser_id
            USING ERRCODE = 'restrict_violation';
    END IF;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "tenancy"."fn_guard_personal_workspace_status"() OWNER TO "postgres";


COMMENT ON FUNCTION "tenancy"."fn_guard_personal_workspace_status"() IS 'BEFORE UPDATE guard on tenancy.workspaces. Blocks status changes away from ''active'' on a personal workspace while the owning lenser profile still exists. Prevents the workspace_not_found error in execute routes that query status=active.';



CREATE OR REPLACE FUNCTION "tenancy"."is_workspace_admin"("p_workspace_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'tenancy', 'lensers', 'public'
    AS $$
    SELECT EXISTS (
        SELECT 1 FROM tenancy.workspace_members wm
        WHERE wm.workspace_id = p_workspace_id
          AND wm.lenser_id = lensers.get_auth_lenser_id()
          AND wm.role IN ('owner', 'admin')
    );
$$;


ALTER FUNCTION "tenancy"."is_workspace_admin"("p_workspace_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "tenancy"."is_workspace_member"("p_workspace_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'tenancy', 'lensers', 'public'
    AS $$
    SELECT EXISTS (
        SELECT 1 FROM tenancy.workspace_members wm
        WHERE wm.workspace_id = p_workspace_id
          AND wm.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "tenancy"."is_workspace_member"("p_workspace_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "tenancy"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'tenancy', 'public'
    AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$;


ALTER FUNCTION "tenancy"."set_updated_at"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "wallet"."credit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text" DEFAULT NULL::"text", "p_reference_id" "uuid" DEFAULT NULL::"uuid", "p_description" "text" DEFAULT NULL::"text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
DECLARE
    v_account_id  uuid;
    v_new_balance bigint;
    v_tx_id       uuid;
BEGIN
    -- Validate credit transaction types
    IF p_tx_type NOT IN ('deposit', 'refund', 'sponsorship_payout', 'adjustment') THEN
        RAISE EXCEPTION 'INVALID_CREDIT_TYPE: % is not a valid credit transaction type', p_tx_type;
    END IF;

    -- Validate amount
    IF p_amount <= 0 THEN
        RAISE EXCEPTION 'INVALID_AMOUNT: amount must be positive, got %', p_amount;
    END IF;

    -- Lock the account row (or create if first deposit)
    SELECT "id" INTO v_account_id
    FROM "wallet"."accounts"
    WHERE "lenser_id" = p_lenser_id
    FOR UPDATE;

    IF v_account_id IS NULL THEN
        -- Auto-create account on first deposit
        INSERT INTO "wallet"."accounts" ("lenser_id", "balance", "lifetime_deposits")
        VALUES (p_lenser_id, 0, 0)
        RETURNING "id" INTO v_account_id;
    ELSE
        -- Check frozen status
        IF (SELECT "frozen" FROM "wallet"."accounts" WHERE "id" = v_account_id) THEN
            RAISE EXCEPTION 'ACCOUNT_FROZEN: wallet account for lenser % is frozen', p_lenser_id;
        END IF;
    END IF;

    -- Update balance and lifetime deposits
    UPDATE "wallet"."accounts"
    SET "balance"           = "balance" + p_amount,
        "lifetime_deposits" = "lifetime_deposits" + p_amount,
        "updated_at"        = now()
    WHERE "id" = v_account_id
    RETURNING "balance" INTO v_new_balance;

    -- Insert transaction record
    INSERT INTO "wallet"."transactions" (
        "account_id", "tx_type", "amount", "direction", "balance_after",
        "reference_type", "reference_id", "description", "metadata"
    )
    VALUES (
        v_account_id, p_tx_type, p_amount, 1, v_new_balance,
        p_reference_type, p_reference_id, p_description, p_metadata
    )
    RETURNING "id" INTO v_tx_id;

    RETURN v_tx_id;
END;
$$;


ALTER FUNCTION "wallet"."credit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."credit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") IS 'Atomically credit a wallet account. Auto-creates account on first deposit. Validates credit types: deposit, refund, sponsorship_payout, adjustment.';



CREATE OR REPLACE FUNCTION "wallet"."debit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text" DEFAULT NULL::"text", "p_reference_id" "uuid" DEFAULT NULL::"uuid", "p_description" "text" DEFAULT NULL::"text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
DECLARE
    v_account_id     uuid;
    v_current_balance bigint;
    v_frozen         boolean;
    v_new_balance    bigint;
    v_tx_id          uuid;
    v_limit_rec      RECORD;
BEGIN
    -- Validate debit transaction types
    IF p_tx_type NOT IN ('spend', 'sponsorship_deposit', 'platform_fee', 'adjustment') THEN
        RAISE EXCEPTION 'INVALID_DEBIT_TYPE: % is not a valid debit transaction type', p_tx_type;
    END IF;

    -- Validate amount
    IF p_amount <= 0 THEN
        RAISE EXCEPTION 'INVALID_AMOUNT: amount must be positive, got %', p_amount;
    END IF;

    -- Lock the account row
    SELECT "id", "balance", "frozen"
    INTO v_account_id, v_current_balance, v_frozen
    FROM "wallet"."accounts"
    WHERE "lenser_id" = p_lenser_id
    FOR UPDATE;

    IF v_account_id IS NULL THEN
        RAISE EXCEPTION 'WALLET_NOT_FOUND: no wallet account for lenser %', p_lenser_id;
    END IF;

    IF v_frozen THEN
        RAISE EXCEPTION 'ACCOUNT_FROZEN: wallet account for lenser % is frozen', p_lenser_id;
    END IF;

    -- Check balance
    IF v_current_balance < p_amount THEN
        RAISE EXCEPTION 'INSUFFICIENT_BALANCE: balance=%, requested=%', v_current_balance, p_amount;
    END IF;

    -- Check spending limits (only for spend type — not for platform fees or adjustments)
    IF p_tx_type = 'spend' THEN
        FOR v_limit_rec IN
            SELECT "id", "period", "limit_credits", "current_usage", "period_start"
            FROM "wallet"."spending_limits"
            WHERE "account_id" = v_account_id
            FOR UPDATE
        LOOP
            -- Check if the period is still active
            IF (v_limit_rec.period = 'daily' AND v_limit_rec.period_start > now() - interval '1 day')
                OR (v_limit_rec.period = 'monthly' AND v_limit_rec.period_start > now() - interval '1 month')
            THEN
                IF v_limit_rec.current_usage + p_amount > v_limit_rec.limit_credits THEN
                    RAISE EXCEPTION 'SPENDING_LIMIT_EXCEEDED: % limit=%, usage=%, requested=%',
                        v_limit_rec.period, v_limit_rec.limit_credits, v_limit_rec.current_usage, p_amount;
                END IF;

                -- Update usage
                UPDATE "wallet"."spending_limits"
                SET "current_usage" = "current_usage" + p_amount,
                    "updated_at"    = now()
                WHERE "id" = v_limit_rec.id;
            END IF;
        END LOOP;
    END IF;

    -- Debit the account
    UPDATE "wallet"."accounts"
    SET "balance"        = "balance" - p_amount,
        "lifetime_spent" = "lifetime_spent" + p_amount,
        "updated_at"     = now()
    WHERE "id" = v_account_id
    RETURNING "balance" INTO v_new_balance;

    -- Insert transaction record
    INSERT INTO "wallet"."transactions" (
        "account_id", "tx_type", "amount", "direction", "balance_after",
        "reference_type", "reference_id", "description", "metadata"
    )
    VALUES (
        v_account_id, p_tx_type, p_amount, -1, v_new_balance,
        p_reference_type, p_reference_id, p_description, p_metadata
    )
    RETURNING "id" INTO v_tx_id;

    RETURN v_tx_id;
END;
$$;


ALTER FUNCTION "wallet"."debit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."debit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") IS 'Atomically debit a wallet account. Enforces balance >= 0 and spending limits. Validates debit types: spend, sponsorship_deposit, platform_fee, adjustment.';



CREATE OR REPLACE FUNCTION "wallet"."get_balance"("p_lenser_id" "uuid") RETURNS bigint
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
  SELECT COALESCE(
    (SELECT balance FROM wallet.accounts WHERE lenser_id = p_lenser_id),
    0
  );
$$;


ALTER FUNCTION "wallet"."get_balance"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."get_balance"("p_lenser_id" "uuid") IS 'Returns current credit balance for a lenser. Returns 0 if no wallet account exists.';



CREATE OR REPLACE FUNCTION "wallet"."get_credits_per_usd"() RETURNS numeric
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
    SELECT COALESCE(
        (SELECT baseline_credits_per_usd
         FROM wallet.credit_valuation_policy
         WHERE singleton = true
         LIMIT 1),
        100  -- safe default
    );
$$;


ALTER FUNCTION "wallet"."get_credits_per_usd"() OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."get_credits_per_usd"() IS 'Returns the current baseline credits-per-USD rate from wallet.credit_valuation_policy. Falls back to 100 if no policy row exists.';



CREATE OR REPLACE FUNCTION "wallet"."get_wallet_summary"("p_lenser_id" "uuid") RETURNS TABLE("available_balance" bigint, "pending_reserved" bigint, "total_balance" bigint, "lifetime_deposits" bigint, "lifetime_spent" bigint, "remaining_pct" numeric)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
    SELECT
        a.balance                                                           AS available_balance,
        COALESCE(SUM(pc.reserved_amount), 0)::bigint                       AS pending_reserved,
        (a.balance + COALESCE(SUM(pc.reserved_amount), 0))::bigint         AS total_balance,
        a.lifetime_deposits,
        a.lifetime_spent,
        CASE
            WHEN a.lifetime_deposits = 0 THEN 100.00
            ELSE ROUND((a.balance::numeric / a.lifetime_deposits::numeric) * 100, 2)
        END                                                                 AS remaining_pct
    FROM wallet.accounts a
    LEFT JOIN wallet.pending_charges pc
           ON pc.account_id = a.id
          AND pc.status = 'reserved'
    WHERE a.lenser_id = p_lenser_id
    GROUP BY a.id, a.balance, a.lifetime_deposits, a.lifetime_spent;
$$;


ALTER FUNCTION "wallet"."get_wallet_summary"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."get_wallet_summary"("p_lenser_id" "uuid") IS 'Returns enriched wallet balance for a lenser: available_balance (spendable), pending_reserved (held), total_balance, lifetime_deposits, lifetime_spent, remaining_pct (available as % of deposits). Returns no rows if the lenser has no wallet account.';



CREATE OR REPLACE FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
DECLARE
    v_charge      "wallet"."pending_charges"%ROWTYPE;
    v_new_balance bigint;
BEGIN
    -- Lock and fetch pending charge
    SELECT * INTO v_charge
    FROM "wallet"."pending_charges"
    WHERE "id" = p_reservation_id
    FOR UPDATE;

    IF v_charge.id IS NULL THEN
        RAISE EXCEPTION 'RESERVATION_NOT_FOUND: reservation % does not exist', p_reservation_id;
    END IF;

    IF v_charge.status != 'reserved'::charge_status_enum THEN
        RAISE EXCEPTION 'RESERVATION_ALREADY_FINALIZED: reservation % is already % (not reserved)',
            p_reservation_id, v_charge.status;
    END IF;

    -- Full refund
    UPDATE "wallet"."accounts"
    SET "balance" = "balance" + v_charge.reserved_amount,
        "updated_at" = now()
    WHERE "id" = v_charge.account_id
    RETURNING "balance" INTO v_new_balance;

    -- Insert refund transaction
    INSERT INTO "wallet"."transactions" (
        "account_id", "tx_type", "amount", "direction", "balance_after",
        "reference_type", "reference_id", "description"
    )
    VALUES (
        v_charge.account_id, 'refund'::wallet.transaction_type_enum,
        v_charge.reserved_amount, 1, v_new_balance,
        'wallet.pending_charges'::text, p_reservation_id,
        'Full refund on execution failure'
    );

    -- Mark charge as released
    UPDATE "wallet"."pending_charges"
    SET "status" = 'released'::charge_status_enum,
        "settled_at" = now()
    WHERE "id" = p_reservation_id;
END;
$$;


ALTER FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid") IS 'Fully refund reservation on execution failure. Raises: RESERVATION_NOT_FOUND, RESERVATION_ALREADY_FINALIZED.';



CREATE OR REPLACE FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid", "p_expected_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
DECLARE
    v_charge      "wallet"."pending_charges"%ROWTYPE;
    v_new_balance bigint;
BEGIN
    -- Lock and fetch pending charge
    SELECT * INTO v_charge
    FROM "wallet"."pending_charges"
    WHERE "id" = p_reservation_id
    FOR UPDATE;

    IF v_charge.id IS NULL THEN
        RAISE EXCEPTION 'RESERVATION_NOT_FOUND: reservation % does not exist', p_reservation_id;
    END IF;

    -- Ownership check (optional but enforced when caller supplies a lenser_id)
    IF p_expected_lenser_id IS NOT NULL
       AND v_charge.lenser_id IS DISTINCT FROM p_expected_lenser_id THEN
        RAISE EXCEPTION 'UNAUTHORIZED_RELEASE: reservation % does not belong to lenser %',
            p_reservation_id, p_expected_lenser_id;
    END IF;

    IF v_charge.status != 'reserved'::charge_status_enum THEN
        RAISE EXCEPTION 'RESERVATION_ALREADY_FINALIZED: reservation % is already % (not reserved)',
            p_reservation_id, v_charge.status;
    END IF;

    -- Full refund
    UPDATE "wallet"."accounts"
    SET "balance" = "balance" + v_charge.reserved_amount,
        "updated_at" = now()
    WHERE "id" = v_charge.account_id
    RETURNING "balance" INTO v_new_balance;

    -- Insert refund transaction
    INSERT INTO "wallet"."transactions" (
        "account_id", "tx_type", "amount", "direction", "balance_after",
        "reference_type", "reference_id", "description"
    )
    VALUES (
        v_charge.account_id, 'refund'::wallet.transaction_type_enum,
        v_charge.reserved_amount, 1, v_new_balance,
        'wallet.pending_charges'::text, p_reservation_id,
        'Full refund on execution failure'
    );

    -- Mark charge as released
    UPDATE "wallet"."pending_charges"
    SET "status" = 'released'::charge_status_enum,
        "settled_at" = now()
    WHERE "id" = p_reservation_id;
END;
$$;


ALTER FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid", "p_expected_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid", "p_expected_lenser_id" "uuid") IS 'Fully refund reservation on execution failure. p_expected_lenser_id: when supplied, raises UNAUTHORIZED_RELEASE if the reservation owner does not match — prevents cross-user refund manipulation with a leaked platform key. Raises: RESERVATION_NOT_FOUND, RESERVATION_ALREADY_FINALIZED, UNAUTHORIZED_RELEASE.';



CREATE OR REPLACE FUNCTION "wallet"."reserve_credits"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "uuid", "p_description" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
DECLARE
    v_account_id     uuid;
    v_reservation_id uuid;
    v_new_balance    bigint;
BEGIN
    -- Validate amount
    IF p_amount <= 0 THEN
        RAISE EXCEPTION 'INVALID_AMOUNT: amount must be positive, got %', p_amount;
    END IF;

    -- Lock the account row (must exist)
    SELECT "id" INTO v_account_id
    FROM "wallet"."accounts"
    WHERE "lenser_id" = p_lenser_id
    FOR UPDATE;

    IF v_account_id IS NULL THEN
        RAISE EXCEPTION 'WALLET_NOT_FOUND: no wallet for lenser %', p_lenser_id;
    END IF;

    -- Check frozen status
    IF (SELECT "frozen" FROM "wallet"."accounts" WHERE "id" = v_account_id) THEN
        RAISE EXCEPTION 'ACCOUNT_FROZEN: wallet account for lenser % is frozen', p_lenser_id;
    END IF;

    -- Check sufficient balance
    IF (SELECT "balance" FROM "wallet"."accounts" WHERE "id" = v_account_id) < p_amount THEN
        RAISE EXCEPTION 'INSUFFICIENT_BALANCE: requested %, available %',
            p_amount, (SELECT "balance" FROM "wallet"."accounts" WHERE "id" = v_account_id);
    END IF;

    -- Debit account
    UPDATE "wallet"."accounts"
    SET "balance" = "balance" - p_amount,
        "updated_at" = now()
    WHERE "id" = v_account_id
    RETURNING "balance" INTO v_new_balance;

    -- Insert transaction
    INSERT INTO "wallet"."transactions" (
        "account_id", "tx_type", "amount", "direction", "balance_after",
        "reference_type", "reference_id", "description"
    )
    VALUES (
        v_account_id, 'spend'::wallet.transaction_type_enum, p_amount, -1, v_new_balance,
        'wallet.pending_charges'::text, p_reference_id, p_description
    );

    -- Insert pending charge (UNIQUE constraint prevents duplicate)
    INSERT INTO "wallet"."pending_charges" (
        "account_id", "lenser_id", "reserved_amount", "status",
        "reference_id", "description"
    )
    VALUES (
        v_account_id, p_lenser_id, p_amount, 'reserved'::charge_status_enum,
        p_reference_id, p_description
    )
    RETURNING "id" INTO v_reservation_id;

    RETURN v_reservation_id;
END;
$$;


ALTER FUNCTION "wallet"."reserve_credits"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "uuid", "p_description" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."reserve_credits"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "uuid", "p_description" "text") IS 'Atomically debit credits and create reservation. Returns reservation_id (UUID). Raises: WALLET_NOT_FOUND, ACCOUNT_FROZEN, INSUFFICIENT_BALANCE.';



CREATE OR REPLACE FUNCTION "wallet"."reserve_credits_safe"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "text", "p_description" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
declare
  v_ref_uuid uuid;
begin
  begin
    v_ref_uuid := p_reference_id::uuid;
  exception when invalid_text_representation then
    raise exception 'INVALID_REFERENCE_ID: reference_id must be a valid UUID, got: %', p_reference_id;
  end;

  return "wallet"."reserve_credits"(p_lenser_id, p_amount, v_ref_uuid, p_description);
end;
$$;


ALTER FUNCTION "wallet"."reserve_credits_safe"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "text", "p_description" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."reserve_credits_safe"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "text", "p_description" "text") IS 'Wrapper around reserve_credits that validates p_reference_id is a valid UUID before casting. Use this from application code to get a clear error on malformed reference IDs.';



CREATE OR REPLACE FUNCTION "wallet"."reset_spending_period"("p_period" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet'
    AS $$
BEGIN
    IF p_period NOT IN ('daily', 'monthly') THEN
        RAISE EXCEPTION 'INVALID_PERIOD: must be daily or monthly, got %', p_period;
    END IF;

    UPDATE "wallet"."spending_limits"
    SET "current_usage" = 0,
        "period_start"  = now(),
        "updated_at"    = now()
    WHERE "period" = p_period
      AND (
          (p_period = 'daily'   AND "period_start" <= now() - interval '1 day')
          OR
          (p_period = 'monthly' AND "period_start" <= now() - interval '1 month')
      );
END;
$$;


ALTER FUNCTION "wallet"."reset_spending_period"("p_period" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."reset_spending_period"("p_period" "text") IS 'Resets current_usage to 0 for all spending limits where the period has elapsed. Called by pg_cron.';



CREATE OR REPLACE FUNCTION "wallet"."settle_charge"("p_reservation_id" "uuid", "p_actual_amount" bigint) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet', 'execution'
    AS $$
DECLARE
    v_charge       wallet.pending_charges%ROWTYPE;
    v_delta        bigint;
    v_new_balance  bigint;
    v_overrun      bigint;
BEGIN
    -- Validate actual amount
    IF p_actual_amount < 0 THEN
        RAISE EXCEPTION 'INVALID_AMOUNT: actual amount must be non-negative, got %', p_actual_amount;
    END IF;

    -- Lock and fetch pending charge
    SELECT * INTO v_charge
    FROM wallet.pending_charges
    WHERE id = p_reservation_id
    FOR UPDATE;

    IF v_charge.id IS NULL THEN
        RAISE EXCEPTION 'RESERVATION_NOT_FOUND: reservation % does not exist', p_reservation_id;
    END IF;

    IF v_charge.status != 'reserved'::wallet.charge_status_enum THEN
        RAISE EXCEPTION 'RESERVATION_ALREADY_FINALIZED: reservation % is already % (not reserved)',
            p_reservation_id, v_charge.status;
    END IF;

    -- Detect cost overrun (actual > reserved) before capping.
    -- This means the token estimation was too low — tag for monitoring.
    v_overrun := p_actual_amount - v_charge.reserved_amount;
    IF v_overrun > 0 THEN
        RAISE WARNING 'COST_OVERRUN: reservation % actual=% reserved=% overrun=%',
            p_reservation_id, p_actual_amount, v_charge.reserved_amount, v_overrun;

        -- Tag the overrun run for monitoring (best-effort, non-blocking)
        BEGIN
            INSERT INTO execution.execution_tags (run_id, tag, severity, metadata)
            VALUES (
                p_reservation_id,
                'cost_overrun',
                'warn',
                jsonb_build_object(
                    'reserved_credits', v_charge.reserved_amount,
                    'actual_credits',   p_actual_amount,
                    'overrun_credits',  v_overrun
                )
            );
        EXCEPTION WHEN OTHERS THEN
            -- run_id FK may not match execution.runs; ignore silently
            NULL;
        END;
    END IF;

    -- Cap actual at reserved (user never pays more than they reserved)
    p_actual_amount := LEAST(p_actual_amount, v_charge.reserved_amount);

    -- Calculate refund delta
    v_delta := v_charge.reserved_amount - p_actual_amount;

    -- If refund needed, credit back to account
    IF v_delta > 0 THEN
        UPDATE wallet.accounts
        SET balance    = balance + v_delta,
            updated_at = now()
        WHERE id = v_charge.account_id
        RETURNING balance INTO v_new_balance;

        -- Insert refund transaction
        INSERT INTO wallet.transactions (
            account_id, tx_type, amount, direction, balance_after,
            reference_type, reference_id, description
        )
        VALUES (
            v_charge.account_id, 'refund'::wallet.transaction_type_enum,
            v_delta, 1, v_new_balance,
            'wallet.pending_charges'::text, p_reservation_id,
            'Refund from charge settlement'
        );
    END IF;

    -- Update lifetime_spent with the amount actually charged (not the reserved amount)
    UPDATE wallet.accounts
    SET lifetime_spent = lifetime_spent + p_actual_amount,
        updated_at     = now()
    WHERE id = v_charge.account_id;

    -- Mark charge as settled
    UPDATE wallet.pending_charges
    SET status         = 'settled'::wallet.charge_status_enum,
        settled_amount = p_actual_amount,
        settled_at     = now()
    WHERE id = p_reservation_id;
END;
$$;


ALTER FUNCTION "wallet"."settle_charge"("p_reservation_id" "uuid", "p_actual_amount" bigint) OWNER TO "postgres";


COMMENT ON FUNCTION "wallet"."settle_charge"("p_reservation_id" "uuid", "p_actual_amount" bigint) IS 'Finalize charge with actual amount. Refunds delta if actual < reserved. Updates lifetime_spent on wallet.accounts with the settled amount. Logs a cost_overrun tag to execution.execution_tags if actual > reserved. Raises: RESERVATION_NOT_FOUND, RESERVATION_ALREADY_FINALIZED.';



CREATE OR REPLACE FUNCTION "wallet"."trg_auto_create_account"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'wallet', 'lensers'
    AS $$
BEGIN
  INSERT INTO wallet.accounts (lenser_id)
  VALUES (NEW.id)
  ON CONFLICT (lenser_id) DO NOTHING;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "wallet"."trg_auto_create_account"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "wallet"."trg_transactions_immutable"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'wallet'
    AS $$
BEGIN
    IF TG_OP = 'DELETE' THEN
        RAISE EXCEPTION 'Wallet transactions are immutable. Deletion is not permitted.';
    ELSIF TG_OP = 'UPDATE' THEN
        RAISE EXCEPTION 'Wallet transactions are immutable. Updates are not permitted.';
    END IF;
    RETURN NULL;
END;
$$;


ALTER FUNCTION "wallet"."trg_transactions_immutable"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "wallet"."trg_verify_balance_after"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
    v_current_balance bigint;
    v_expected_balance bigint;
BEGIN
    -- Get the current account balance
    SELECT balance INTO v_current_balance
    FROM wallet.accounts
    WHERE id = NEW.account_id
    FOR UPDATE;  -- lock row to prevent concurrent corruption

    IF v_current_balance IS NULL THEN
        RAISE EXCEPTION 'WALLET_ACCOUNT_NOT_FOUND: No wallet account with id=%', NEW.account_id;
    END IF;

    -- Expected balance = current balance + (amount * direction)
    -- Note: the wallet functions update the account balance BEFORE inserting the
    -- transaction, so current balance should already reflect this transaction.
    -- We verify balance_after matches the current (already-updated) balance.
    IF NEW.balance_after <> v_current_balance THEN
        RAISE EXCEPTION 'BALANCE_MISMATCH: balance_after (%) does not match account balance (%) for account_id=%. '
            'This indicates a bug in the wallet function that inserted this transaction.',
            NEW.balance_after, v_current_balance, NEW.account_id;
    END IF;

    RETURN NEW;
END;
$$;


ALTER FUNCTION "wallet"."trg_verify_balance_after"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."apply"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000000'::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'lensers', 'content', 'public', 'auth'
    AS $$
DECLARE
  v_rule          RECORD;
  v_multiplier    numeric(4,2);
  v_effective_xp  integer;
  v_daily_xp      integer;
  v_daily_events  integer;
  v_season_xp     integer;
  v_last_event    timestamptz;
  v_now           timestamptz := now();
  v_new_total     bigint;
  v_new_level     integer;
  v_season_id     uuid;
BEGIN
  -- Serialize concurrent XP awards for the same lenser+app
  PERFORM pg_advisory_xact_lock(hashtext(p_lenser_id::text || '/' || p_app_id::text));

  -------------------------------------------------------------------
  -- Load rule
  -------------------------------------------------------------------
  SELECT *
  INTO v_rule
  FROM xp.rules
  WHERE action_key = p_rule_key
    AND is_active  = true
    AND (app_id = p_app_id OR app_id = '00000000-0000-0000-0000-000000000000'::uuid)
  ORDER BY
    CASE WHEN app_id = p_app_id THEN 0 ELSE 1 END
  LIMIT 1;

  IF v_rule IS NULL THEN
    RETURN;
  END IF;

  -------------------------------------------------------------------
  -- Fetch active season early (needed for cap check + event insert)
  -------------------------------------------------------------------
  v_season_id := xp.get_active_season_id(p_app_id);

  -------------------------------------------------------------------
  -- Compute effective XP with difficulty multiplier
  -------------------------------------------------------------------
  SELECT COALESCE(multiplier, 1.0)
  INTO v_multiplier
  FROM xp.difficulty_multipliers
  WHERE difficulty = v_rule.difficulty;

  IF v_multiplier IS NULL THEN
    v_multiplier := 1.0;
  END IF;

  v_effective_xp := CEIL(v_rule.base_xp * v_multiplier);

  -------------------------------------------------------------------
  -- Rule-level cooldown
  -------------------------------------------------------------------
  IF v_rule.cooldown_seconds IS NOT NULL THEN
    SELECT created_at
    INTO v_last_event
    FROM xp.events
    WHERE lenser_id  = p_lenser_id
      AND action_key = p_rule_key
      AND app_id     = p_app_id
    ORDER BY created_at DESC
    LIMIT 1;

    IF v_last_event IS NOT NULL
       AND EXTRACT(EPOCH FROM (v_now - v_last_event)) < v_rule.cooldown_seconds THEN
      RETURN;
    END IF;
  END IF;

  -------------------------------------------------------------------
  -- Max events per day
  -------------------------------------------------------------------
  IF v_rule.max_events_per_day IS NOT NULL THEN
    SELECT COUNT(*)
    INTO v_daily_events
    FROM xp.events
    WHERE lenser_id  = p_lenser_id
      AND action_key = p_rule_key
      AND app_id     = p_app_id
      AND created_at >= date_trunc('day', v_now);

    IF v_daily_events >= v_rule.max_events_per_day THEN
      RETURN;
    END IF;
  END IF;

  -------------------------------------------------------------------
  -- Max XP per day
  -------------------------------------------------------------------
  IF v_rule.max_xp_per_day IS NOT NULL THEN
    SELECT COALESCE(SUM(xp), 0)
    INTO v_daily_xp
    FROM xp.events
    WHERE lenser_id  = p_lenser_id
      AND action_key = p_rule_key
      AND app_id     = p_app_id
      AND created_at >= date_trunc('day', v_now);

    IF v_daily_xp + v_effective_xp > v_rule.max_xp_per_day THEN
      RETURN;
    END IF;
  END IF;

  -------------------------------------------------------------------
  -- Season XP cap (FIX: filter by current season, not all-time)
  -------------------------------------------------------------------
  IF v_rule.max_xp_per_season IS NOT NULL AND v_season_id IS NOT NULL THEN
    SELECT COALESCE(SUM(xp), 0)
    INTO v_season_xp
    FROM xp.events
    WHERE lenser_id  = p_lenser_id
      AND action_key = p_rule_key
      AND app_id     = p_app_id
      AND season_id  = v_season_id;

    IF v_season_xp + v_effective_xp > v_rule.max_xp_per_season THEN
      RETURN;
    END IF;
  END IF;

  -------------------------------------------------------------------
  -- Insert event (with season attribution)
  -------------------------------------------------------------------
  INSERT INTO xp.events (
    id, lenser_id, app_id, rule_id, action_key,
    xp, base_xp, source, source_ref_type, source_ref_id,
    season_id, created_at
  )
  VALUES (
    gen_random_uuid(),
    p_lenser_id,
    p_app_id,
    v_rule.id,
    p_rule_key,
    v_effective_xp,
    v_rule.base_xp,
    p_source,
    p_source_ref_type,
    p_source_ref_id,
    v_season_id,
    v_now
  );

  -------------------------------------------------------------------
  -- Update totals + current_level
  -------------------------------------------------------------------
  INSERT INTO xp.totals (lenser_id, app_id, total_xp, current_level)
  VALUES (p_lenser_id, p_app_id, v_effective_xp, 1)
  ON CONFLICT (lenser_id, app_id)
  DO UPDATE SET
    total_xp = xp.totals.total_xp + EXCLUDED.total_xp,
    updated_at = v_now;

  SELECT total_xp INTO v_new_total
  FROM xp.totals
  WHERE lenser_id = p_lenser_id AND app_id = p_app_id;

  v_new_level := xp.compute_level_from(p_app_id, v_new_total);

  UPDATE xp.totals
  SET current_level = v_new_level
  WHERE lenser_id = p_lenser_id AND app_id = p_app_id
    AND current_level IS DISTINCT FROM v_new_level;

  -------------------------------------------------------------------
  -- Update season totals
  -------------------------------------------------------------------
  PERFORM xp.update_season_totals(p_app_id, p_lenser_id, v_effective_xp::bigint);

  -------------------------------------------------------------------
  -- Update streak (if rule has streak_type)
  -------------------------------------------------------------------
  IF v_rule.streak_type IS NOT NULL THEN
    PERFORM xp.update_daily_streak(
      p_lenser_id,
      p_app_id,
      v_rule.streak_type,
      v_now::date
    );
  END IF;

END;
$$;


ALTER FUNCTION "xp"."apply"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."auto_create_next_season"("p_app_id" "uuid", "p_duration_days" integer DEFAULT 90) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
DECLARE
  v_latest       RECORD;
  v_new_id       uuid;
  v_season_num   int;
  v_app_slug     text;
  v_new_slug     text;
  v_starts_at    timestamptz;
BEGIN
  -- Prevent concurrent season creation for same app
  PERFORM pg_advisory_xact_lock(hashtext('season_create/' || p_app_id::text));

  -- Get the latest season for this app (active or not)
  SELECT id, starts_at, ends_at, is_active, slug
  INTO v_latest
  FROM xp.seasons
  WHERE app_id = p_app_id
  ORDER BY ends_at DESC
  LIMIT 1;

  -- If active season not yet ended, nothing to do
  IF v_latest IS NOT NULL AND v_latest.is_active AND v_latest.ends_at > now() THEN
    RETURN NULL;
  END IF;

  -- Deactivate the latest season if it was still marked active
  IF v_latest IS NOT NULL AND v_latest.is_active THEN
    UPDATE xp.seasons SET is_active = false WHERE id = v_latest.id;

    -- Audit: season closed
    INSERT INTO audit.events (event_type, entity_schema, entity_table, entity_id, actor_type, payload, severity)
    VALUES (
      'season.closed', 'xp', 'seasons', v_latest.id, 'system',
      jsonb_build_object('app_id', p_app_id, 'slug', v_latest.slug, 'ended_at', v_latest.ends_at),
      'info'
    );
  END IF;

  -- Compute next season number
  SELECT COUNT(*) + 1 INTO v_season_num
  FROM xp.seasons WHERE app_id = p_app_id;

  -- Canonical slug: s{N}_{app_slug}
  SELECT slug INTO v_app_slug FROM xp.apps WHERE id = p_app_id;
  v_new_slug := 's' || v_season_num || '_' || COALESCE(v_app_slug, 'app');

  -- Start immediately after previous, or now if gap > 7 days
  IF v_latest IS NOT NULL AND (now() - v_latest.ends_at) < interval '7 days' THEN
    v_starts_at := v_latest.ends_at;
  ELSE
    v_starts_at := now();
  END IF;

  -- Create the new season
  INSERT INTO xp.seasons (id, app_id, slug, name, starts_at, ends_at, is_active)
  VALUES (
    gen_random_uuid(),
    p_app_id,
    v_new_slug,
    initcap(replace(v_new_slug, '_', ' ')),
    v_starts_at,
    v_starts_at + (p_duration_days || ' days')::interval,
    true
  )
  RETURNING id INTO v_new_id;

  -- Audit: season opened
  INSERT INTO audit.events (event_type, entity_schema, entity_table, entity_id, actor_type, payload, severity)
  VALUES (
    'season.opened', 'xp', 'seasons', v_new_id, 'system',
    jsonb_build_object('app_id', p_app_id, 'slug', v_new_slug, 'starts_at', v_starts_at),
    'info'
  );

  RAISE NOTICE 'Created season % for app %', v_new_slug, p_app_id;
  RETURN v_new_id;
END;
$$;


ALTER FUNCTION "xp"."auto_create_next_season"("p_app_id" "uuid", "p_duration_days" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."check_all_seasons"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
DECLARE
  v_app RECORD;
BEGIN
  FOR v_app IN
    SELECT id FROM xp.apps WHERE is_active = true
  LOOP
    PERFORM xp.auto_create_next_season(v_app.id);
  END LOOP;
END;
$$;


ALTER FUNCTION "xp"."check_all_seasons"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."check_and_rollover_season"("p_app_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$DECLARE
  caller text;
  ended_season_count int;
BEGIN
  -- Extract caller safely (NULL during schema introspection)
  BEGIN
    caller := current_setting('request.jwt.claim.sub', true);
  EXCEPTION WHEN OTHERS THEN
    caller := NULL;
  END;

  -- Only block real external calls, not Supabase system processes
  IF caller IS NOT NULL AND caller <> 'service_role' THEN
      RAISE EXCEPTION 'Unauthorized: only service_role can call this function';
  END IF;

  -- Check for finished seasons
  SELECT COUNT(*) INTO ended_season_count
  FROM xp.seasons
  WHERE app_id = p_app_id
    AND is_active = true
    AND ends_at < now();

  -- Rollover
  IF ended_season_count > 0 THEN
    PERFORM xp.rollover_season(p_app_id);
  END IF;

END;$$;


ALTER FUNCTION "xp"."check_and_rollover_season"("p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."compute_level_from"("p_app_id" "uuid", "p_total_xp" bigint) RETURNS integer
    LANGUAGE "sql"
    SET "search_path" TO 'xp', 'public'
    AS $$SELECT COALESCE(
    (
      SELECT level
      FROM xp.levels
      WHERE app_id = p_app_id
        AND p_total_xp >= min_total_xp
        AND p_total_xp < max_total_xp
      ORDER BY level DESC
      LIMIT 1
    ),
    1
  );$$;


ALTER FUNCTION "xp"."compute_level_from"("p_app_id" "uuid", "p_total_xp" bigint) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."fn_refresh_monthly_rollup"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  INSERT INTO xp.monthly_rollup (lenser_id, year, month, xp)
  SELECT
    lenser_id,
    EXTRACT(YEAR FROM created_at)::int,
    EXTRACT(MONTH FROM created_at)::int,
    SUM(xp)
  FROM xp.events
  WHERE lenser_id IS NOT NULL
  GROUP BY lenser_id, EXTRACT(YEAR FROM created_at)::int, EXTRACT(MONTH FROM created_at)::int
  ON CONFLICT (lenser_id, year, month)
  DO UPDATE SET xp = EXCLUDED.xp;
END;
$$;


ALTER FUNCTION "xp"."fn_refresh_monthly_rollup"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."fn_trigger_badge_check"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  PERFORM public.fn_badge_check_and_award(NEW.lenser_id);
  RETURN NEW;
EXCEPTION WHEN OTHERS THEN
  RAISE NOTICE 'fn_trigger_badge_check: % (lenser=%)', SQLERRM, NEW.lenser_id;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."fn_trigger_badge_check"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."fn_xp_auto_activate_seasons"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
DECLARE
  v_deactivated INT := 0;
  v_activated   INT := 0;
BEGIN
  -- 1. Deactivate expired seasons
  UPDATE xp.seasons
  SET is_active = false
  WHERE is_active = true
    AND ends_at IS NOT NULL
    AND ends_at < now();
  GET DIAGNOSTICS v_deactivated = ROW_COUNT;

  -- 2. Activate seasons that should now be running (starts_at passed, not ended,
  --    and no currently-active season exists for that app).
  --    Process one candidate per app at a time (oldest starts_at first) to avoid
  --    violating the no-overlap exclusion constraint.
  WITH candidates AS (
    SELECT
      s.id,
      s.app_id,
      ROW_NUMBER() OVER (PARTITION BY s.app_id ORDER BY s.starts_at) AS rn
    FROM xp.seasons s
    WHERE s.is_active = false
      AND s.starts_at <= now()
      AND (s.ends_at IS NULL OR s.ends_at > now())
      AND NOT EXISTS (
        SELECT 1 FROM xp.seasons a
        WHERE a.app_id   = s.app_id
          AND a.is_active = true
      )
  )
  UPDATE xp.seasons s
  SET is_active = true
  FROM candidates c
  WHERE c.id = s.id
    AND c.rn = 1;
  GET DIAGNOSTICS v_activated = ROW_COUNT;

  IF v_deactivated > 0 OR v_activated > 0 THEN
    RAISE NOTICE 'fn_xp_auto_activate_seasons: deactivated=%, activated=%', v_deactivated, v_activated;
  END IF;
END;
$$;


ALTER FUNCTION "xp"."fn_xp_auto_activate_seasons"() OWNER TO "postgres";


COMMENT ON FUNCTION "xp"."fn_xp_auto_activate_seasons"() IS 'CB-P2: Deactivates expired seasons and activates ready ones. Called by pg_cron. Idempotent and advisory-locked via automation.fn_run_with_lock.';



CREATE OR REPLACE FUNCTION "xp"."fn_xp_auto_activate_seasons_safe"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'automation', 'public'
    AS $$
BEGIN
  PERFORM automation.fn_run_with_lock(
    'xp-auto-activate-seasons',
    'SELECT xp.fn_xp_auto_activate_seasons()'
  );
END;
$$;


ALTER FUNCTION "xp"."fn_xp_auto_activate_seasons_safe"() OWNER TO "postgres";


COMMENT ON FUNCTION "xp"."fn_xp_auto_activate_seasons_safe"() IS 'CB-P2: Advisory-lock wrapper for fn_xp_auto_activate_seasons. Called by pg_cron every hour.';



CREATE OR REPLACE FUNCTION "xp"."generate_levels"("p_app_id" "uuid", "p_max_level" integer DEFAULT 200, "p_base_xp" integer DEFAULT 100) RETURNS "void"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'public'
    AS $$DECLARE
    lvl int;
    min_xp bigint;
    max_xp bigint;
BEGIN
    -- Clear old levels for this app
    DELETE FROM xp.levels WHERE app_id = p_app_id;

    min_xp := 0;

    FOR lvl IN 1..p_max_level LOOP

        -- Level XP curve:
        -- Low levels = fast progression
        -- Mid levels = moderate
        -- High levels = exponential
        max_xp := min_xp + (p_base_xp * lvl * lvl / 5);

        INSERT INTO xp.levels (app_id, level, min_total_xp, max_total_xp, metadata)
        VALUES (
            p_app_id,
            lvl,
            min_xp,
            max_xp,
            jsonb_build_object('generated', true, 'formula', 'quadratic_scaled')
        );

        min_xp := max_xp + 1;
    END LOOP;

END;$$;


ALTER FUNCTION "xp"."generate_levels"("p_app_id" "uuid", "p_max_level" integer, "p_base_xp" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."get_active_season_id"("p_app_id" "uuid") RETURNS "uuid"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'xp', 'public'
    AS $$
  SELECT id
  FROM xp.seasons
  WHERE app_id = p_app_id
    AND is_active = true
    AND now() BETWEEN starts_at AND ends_at
  LIMIT 1;
$$;


ALTER FUNCTION "xp"."get_active_season_id"("p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."grant_contribution_xp"("p_lenser_id" "uuid", "p_context" "xp"."contribution_context_enum", "p_contribution_type" "text", "p_external_ref" "text" DEFAULT NULL::"text", "p_title" "text" DEFAULT NULL::"text", "p_metadata" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$
DECLARE
  v_rule_key    text;
  v_event_id    uuid;
  v_contrib_id  uuid;
BEGIN
  -- Map (context, contribution_type) to rule key
  CASE
    WHEN p_contribution_type = 'pr_merged' AND p_context = 'main_project' THEN
      v_rule_key := 'CONTRIB_PR_MERGED_MAIN';
    WHEN p_contribution_type = 'pr_merged' AND p_context = 'community_plugin' THEN
      v_rule_key := 'CONTRIB_PR_MERGED_COMMUNITY';
    WHEN p_contribution_type = 'pr_merged' AND p_context = 'documentation' THEN
      v_rule_key := 'CONTRIB_PR_MERGED_DOCS';
    WHEN p_contribution_type = 'pr_merged' AND p_context = 'infrastructure' THEN
      v_rule_key := 'CONTRIB_PR_MERGED_COMMUNITY';  -- treat infra same as community
    WHEN p_contribution_type = 'issue_filed' THEN
      v_rule_key := 'CONTRIB_ISSUE_FILED';
    WHEN p_contribution_type = 'review_given' THEN
      v_rule_key := 'CONTRIB_REVIEW_GIVEN';
    ELSE
      RAISE EXCEPTION 'Unknown contribution type: % / %', p_context, p_contribution_type;
  END CASE;

  -- Award XP via the standard pipeline (source = 'contribution', tracked under forum app)
  PERFORM xp.apply(
    p_lenser_id       := p_lenser_id,
    p_rule_key        := v_rule_key,
    p_source          := 'contribution'::xp.source_enum,
    p_source_ref_type := 'contribution',
    p_source_ref_id   := NULL,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );

  -- Fetch the event just created (most recent for this lenser + rule)
  SELECT id INTO v_event_id
  FROM xp.events
  WHERE lenser_id  = p_lenser_id
    AND action_key = v_rule_key
  ORDER BY created_at DESC
  LIMIT 1;

  -- Record the contribution
  INSERT INTO xp.contributions (
    lenser_id, context, contribution_type,
    external_ref, title, verified_by,
    xp_event_id, metadata
  )
  VALUES (
    p_lenser_id, p_context, p_contribution_type,
    p_external_ref, p_title, 'system',
    v_event_id, p_metadata
  )
  RETURNING id INTO v_contrib_id;

  RETURN v_contrib_id;
END;
$$;


ALTER FUNCTION "xp"."grant_contribution_xp"("p_lenser_id" "uuid", "p_context" "xp"."contribution_context_enum", "p_contribution_type" "text", "p_external_ref" "text", "p_title" "text", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."mark_event_verified"("p_event_id" "uuid", "p_ai_verified" boolean, "p_verifier" "text", "p_details" "jsonb" DEFAULT NULL::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'public'
    AS $$BEGIN
  INSERT INTO xp.event_verifications (
    event_id, ai_verified, verifier, details, created_at
  )
  VALUES (
    p_event_id, p_ai_verified, p_verifier, p_details, now()
  );
END;$$;


ALTER FUNCTION "xp"."mark_event_verified"("p_event_id" "uuid", "p_ai_verified" boolean, "p_verifier" "text", "p_details" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_battle_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'battles'
    AS $$
BEGIN
  -- On INSERT: skip if battle starts as draft
  -- On UPDATE: WHEN clause ensures OLD.status = 'draft' AND NEW.status != 'draft'
  IF TG_OP = 'INSERT' AND NEW.status = 'draft'::battles.battle_status_enum THEN
    RETURN NEW;
  END IF;

  -- Guard against double-award on UPDATE if XP was already given at INSERT
  IF TG_OP = 'UPDATE' THEN
    IF EXISTS (
      SELECT 1 FROM xp.events
      WHERE source_ref_type = 'battle'
        AND source_ref_id   = NEW.id
        AND lenser_id       = NEW.creator_lenser_id
        AND action_key      = 'BATTLE_CREATED'
        AND xp              > 0
    ) THEN
      RETURN NEW;
    END IF;
  END IF;

  PERFORM xp.apply(
    p_lenser_id       := NEW.creator_lenser_id,
    p_rule_key        := 'BATTLE_CREATED',
    p_source          := 'battle'::xp.source_enum,
    p_source_ref_type := 'battle',
    p_source_ref_id   := NEW.id,
    p_app_id          := '00000000-0000-0000-0000-000000000002'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_battle_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_lens_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lenses', 'content'
    AS $$
BEGIN
  -- Only award creation XP for publicly visible, published lenses
  IF NEW.visibility <> 'public'::content.visibility_enum
     OR NEW.status   <> 'published'::content.content_status THEN
    RETURN NEW;
  END IF;

  PERFORM xp.apply(
    p_lenser_id       := NEW.lenser_id,
    p_rule_key        := 'LENS_CREATED',
    p_source          := 'content'::xp.source_enum,
    p_source_ref_type := 'lens',
    p_source_ref_id   := NEW.id,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_lens_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_lens_visibility_changed"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lenses', 'content'
    AS $$
DECLARE
  v_was_public boolean;
  v_now_public boolean;
BEGIN
  v_was_public := OLD.visibility = 'public'::content.visibility_enum
                  AND OLD.status  = 'published'::content.content_status;
  v_now_public := NEW.visibility = 'public'::content.visibility_enum
                  AND NEW.status  = 'published'::content.content_status;

  -- Transition: public+published → non-public → rollback creation XP
  IF v_was_public AND NOT v_now_public THEN
    PERFORM xp.rollback_content_xp('lens', NEW.id, NEW.lenser_id);

  -- Transition: non-public → public+published → re-award if no prior grant
  ELSIF NOT v_was_public AND v_now_public THEN
    IF NOT EXISTS (
      SELECT 1 FROM xp.events
      WHERE source_ref_type = 'lens'
        AND source_ref_id   = NEW.id
        AND lenser_id       = NEW.lenser_id
        AND action_key      = 'LENS_CREATED'
        AND xp              > 0
    ) THEN
      PERFORM xp.apply(
        p_lenser_id       := NEW.lenser_id,
        p_rule_key        := 'LENS_CREATED',
        p_source          := 'content'::xp.source_enum,
        p_source_ref_type := 'lens',
        p_source_ref_id   := NEW.id,
        p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
      );
    END IF;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_lens_visibility_changed"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_reaction_added"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'content', 'lensers', 'public', 'auth'
    AS $$
declare
  v_thread_id uuid;
begin
  -- Resolve thread_id from reaction target
  if new.target_type = 'thread' then
    v_thread_id := new.target_id;

  elsif new.target_type = 'thread_reply' then
    select tr.thread_id
    into v_thread_id
    from content.thread_replies tr
    where tr.id = new.target_id;

  else
    -- prompt_template or others → ignore
    return new;
  end if;

  if v_thread_id is null then
    return new;
  end if;

  insert into analytics.tag_activity_events (
    tag_id,
    entity_type,
    entity_id,
    activity_type,
    actor_id
  )
  select
    tm.tag_id,
    'thread'::content.entity_type_enum,   -- ✅ ONLY trusted enum
    tm.entity_id,
    'reacted',
    new.user_id
  from content.tag_map tm
  where tm.entity_type = 'thread'::content.entity_type_enum
    and tm.entity_id = v_thread_id;

  return new;
end;
$$;


ALTER FUNCTION "xp"."on_reaction_added"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_reaction_given"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp'
    AS $$
BEGIN
  PERFORM xp.apply(
    p_lenser_id       := NEW.lenser_id,
    p_rule_key        := 'REACTION_GIVEN',
    p_source          := 'social'::xp.source_enum,
    p_source_ref_type := 'reaction',
    p_source_ref_id   := NEW.id,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_reaction_given"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_reaction_received"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'content'
    AS $$
DECLARE
  v_owner uuid;
BEGIN
  SELECT lenser_id INTO v_owner
  FROM content.threads
  WHERE id = NEW.target_id;

  -- Self-reaction guard: owner cannot earn REACTION_RECEIVED from their own reaction
  IF v_owner IS NOT NULL AND v_owner <> NEW.lenser_id THEN
    PERFORM xp.apply(
      p_lenser_id       := v_owner,
      p_rule_key        := 'REACTION_RECEIVED',
      p_source          := 'social'::xp.source_enum,
      p_source_ref_type := 'reaction',
      p_source_ref_id   := NEW.id,
      p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
    );
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_reaction_received"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_reply_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp'
    AS $$
BEGIN
  PERFORM xp.apply(
    p_lenser_id       := NEW.lenser_id,
    p_rule_key        := 'THREAD_REPLY_CREATED',
    p_source          := 'social'::xp.source_enum,
    p_source_ref_type := 'thread_reply',
    p_source_ref_id   := NEW.id,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_reply_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_reply_received"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'content'
    AS $$
DECLARE
  v_owner uuid;
BEGIN
  SELECT lenser_id INTO v_owner
  FROM content.threads
  WHERE id = NEW.thread_id;

  -- Self-reply guard: thread owner replying to their own thread earns no RECEIVED XP
  IF v_owner IS NOT NULL AND v_owner <> NEW.lenser_id THEN
    PERFORM xp.apply(
      p_lenser_id       := v_owner,
      p_rule_key        := 'THREAD_REPLY_RECEIVED',
      p_source          := 'social'::xp.source_enum,
      p_source_ref_type := 'thread_reply',
      p_source_ref_id   := NEW.id,
      p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
    );
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_reply_received"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_tag_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'analytics'
    AS $$
BEGIN
  INSERT INTO analytics.tag_activity_events (tag_id, entity_type, entity_id, activity_type)
  VALUES (NEW.tag_id, NEW.entity_type, NEW.entity_id, 'created');
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_tag_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_thread_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'content'
    AS $$
BEGIN
  -- Only award creation XP for publicly visible, published threads
  IF NEW.visibility <> 'public'::content.visibility_enum
     OR NEW.status   <> 'published'::content.content_status THEN
    RETURN NEW;
  END IF;

  PERFORM xp.apply(
    p_lenser_id       := NEW.lenser_id,
    p_rule_key        := 'THREAD_CREATED',
    p_source          := 'content'::xp.source_enum,
    p_source_ref_type := 'thread',
    p_source_ref_id   := NEW.id,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_thread_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_thread_engaged"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp'
    AS $$
BEGIN
  PERFORM xp.apply(
    p_lenser_id       := NEW.viewer_id,
    p_rule_key        := 'THREAD_ENGAGED',
    p_source          := 'social'::xp.source_enum,
    p_source_ref_type := 'thread_view',
    p_source_ref_id   := NEW.thread_id,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_thread_engaged"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_thread_visibility_changed"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'content'
    AS $$
DECLARE
  v_was_public boolean;
  v_now_public boolean;
BEGIN
  v_was_public := OLD.visibility = 'public'::content.visibility_enum
                  AND OLD.status  = 'published'::content.content_status;
  v_now_public := NEW.visibility = 'public'::content.visibility_enum
                  AND NEW.status  = 'published'::content.content_status;

  IF v_was_public AND NOT v_now_public THEN
    PERFORM xp.rollback_content_xp('thread', NEW.id, NEW.lenser_id);

  ELSIF NOT v_was_public AND v_now_public THEN
    IF NOT EXISTS (
      SELECT 1 FROM xp.events
      WHERE source_ref_type = 'thread'
        AND source_ref_id   = NEW.id
        AND lenser_id       = NEW.lenser_id
        AND action_key      = 'THREAD_CREATED'
        AND xp              > 0
    ) THEN
      PERFORM xp.apply(
        p_lenser_id       := NEW.lenser_id,
        p_rule_key        := 'THREAD_CREATED',
        p_source          := 'content'::xp.source_enum,
        p_source_ref_type := 'thread',
        p_source_ref_id   := NEW.id,
        p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
      );
    END IF;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_thread_visibility_changed"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_workflow_created"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lenses'
    AS $$
BEGIN
  -- Only award creation XP for public workflows
  IF NEW.visibility <> 'public' THEN
    RETURN NEW;
  END IF;

  PERFORM xp.apply(
    p_lenser_id       := NEW.lenser_id,
    p_rule_key        := 'WORKFLOW_CREATED',
    p_source          := 'content'::xp.source_enum,
    p_source_ref_type := 'workflow',
    p_source_ref_id   := NEW.id,
    p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
  );
  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_workflow_created"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."on_workflow_visibility_changed"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lenses'
    AS $$
BEGIN
  -- Transition: public → non-public → rollback creation XP
  IF OLD.visibility = 'public' AND NEW.visibility <> 'public' THEN
    PERFORM xp.rollback_content_xp('workflow', NEW.id, NEW.lenser_id);

  -- Transition: non-public → public → re-award if no prior grant
  ELSIF OLD.visibility <> 'public' AND NEW.visibility = 'public' THEN
    IF NOT EXISTS (
      SELECT 1 FROM xp.events
      WHERE source_ref_type = 'workflow'
        AND source_ref_id   = NEW.id
        AND lenser_id       = NEW.lenser_id
        AND action_key      = 'WORKFLOW_CREATED'
        AND xp              > 0
    ) THEN
      PERFORM xp.apply(
        p_lenser_id       := NEW.lenser_id,
        p_rule_key        := 'WORKFLOW_CREATED',
        p_source          := 'content'::xp.source_enum,
        p_source_ref_type := 'workflow',
        p_source_ref_id   := NEW.id,
        p_app_id          := '00000000-0000-0000-0000-000000000001'::uuid
      );
    END IF;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."on_workflow_visibility_changed"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."prevent_event_mutations"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'public', 'auth'
    AS $$DECLARE
  jwt_role text := current_setting('request.jwt.claim.role', true);
BEGIN
  -- Allow system cleanup or post-delete anonymization
  IF jwt_role = 'service_role' OR current_user = 'postgres' THEN
    RETURN NEW;
  END IF;

  RAISE EXCEPTION 'xp_events are immutable';
END;$$;


ALTER FUNCTION "xp"."prevent_event_mutations"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."rollback_content_xp"("p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_lenser_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
DECLARE
  v_event_id uuid;
  v_count    integer := 0;
BEGIN
  -- Find all positive creation XP events for this content item.
  -- Exclude _ROLLBACK suffix entries (already compensated).
  FOR v_event_id IN
    SELECT id
    FROM xp.events
    WHERE source_ref_type = p_source_ref_type
      AND source_ref_id   = p_source_ref_id
      AND lenser_id       = p_lenser_id
      AND xp              > 0
      AND action_key      NOT LIKE '%_ROLLBACK'
  LOOP
    PERFORM xp.rollback_event(v_event_id, 'content_visibility_change');
    v_count := v_count + 1;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "xp"."rollback_content_xp"("p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."rollback_event"("p_event_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS TABLE("total_xp" bigint, "level" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$DECLARE
  v_event xp.events%ROWTYPE;
  v_total_after bigint;
  v_level int;
BEGIN
  -------------------------------------------------------------------
  -- 1) LOAD ORIGINAL EVENT
  -------------------------------------------------------------------
  SELECT *
  INTO v_event
  FROM xp.events e
  WHERE e.id = p_event_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'XP event not found: %', p_event_id;
  END IF;


  -------------------------------------------------------------------
  -- 2) INSERT COMPENSATING NEGATIVE EVENT
  -------------------------------------------------------------------
  INSERT INTO xp.events (
    lenser_id,
    app_id,
    rule_id,
    action_key,
    xp,
    base_xp,
    source,
    source_ref_type,
    source_ref_id,
    signature,
    meta
  )
  VALUES (
    v_event.lenser_id,
    v_event.app_id,
    v_event.rule_id,
    v_event.action_key || '_ROLLBACK',
    -v_event.xp,
    -v_event.base_xp,
    'system',                        -- system-originated rollback
    'xp_event',
    v_event.id,
    NULL,
    jsonb_build_object(
      'rollback_of', v_event.id::text,
      'rollback_reason', p_reason
    )
  );


  -------------------------------------------------------------------
  -- 3) UPDATE XP TOTALS (SUBTRACT ORIGINAL XP)
  -------------------------------------------------------------------
  UPDATE xp.totals t
  SET total_xp = GREATEST(t.total_xp - v_event.xp, 0),
      updated_at = now()
  WHERE t.lenser_id = v_event.lenser_id
    AND t.app_id = v_event.app_id
  RETURNING t.total_xp INTO v_total_after;

  IF NOT FOUND THEN
    -- No totals row: nothing to rollback
    v_total_after := 0;
  END IF;


  -------------------------------------------------------------------
  -- 4) RECALCULATE LEVEL
  -------------------------------------------------------------------
  SELECT lvl.level
  INTO v_level
  FROM xp.levels lvl
  WHERE lvl.app_id = v_event.app_id
    AND v_total_after >= lvl.min_total_xp
    AND v_total_after <= lvl.max_total_xp
  LIMIT 1;

  IF v_level IS NULL THEN
    v_level := 1;
  END IF;


  -------------------------------------------------------------------
  -- 5) UPDATE STORED LEVEL
  -------------------------------------------------------------------
  UPDATE xp.totals
  SET current_level = v_level,
      updated_at = now()
  WHERE lenser_id = v_event.lenser_id
    AND app_id = v_event.app_id;


  -------------------------------------------------------------------
  -- 6) RETURN NEW STATE
  -------------------------------------------------------------------
  RETURN QUERY
  SELECT v_total_after AS total_xp,
         v_level       AS level;

END;$$;


ALTER FUNCTION "xp"."rollback_event"("p_event_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."rollover_season"("p_app_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$
BEGIN
  PERFORM xp.auto_create_next_season(p_app_id, 90);
END;
$$;


ALTER FUNCTION "xp"."rollover_season"("p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."seed_default_curve"("p_app_id" "uuid", "p_max_level" integer DEFAULT 100, "p_base" numeric DEFAULT 50, "p_power" numeric DEFAULT 1.5) RETURNS "void"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'public'
    AS $$BEGIN
  INSERT INTO xp.levels (app_id, level, min_total_xp, max_total_xp)
  SELECT
    p_app_id,
    lvl,
    COALESCE(SUM(increment) OVER (ORDER BY lvl
      ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) AS min_total_xp,
    SUM(increment) OVER (ORDER BY lvl) AS max_total_xp
  FROM (
    SELECT
      level AS lvl,
      CEIL(p_base * power(level::numeric, p_power))::bigint AS increment
    FROM generate_series(1, p_max_level) AS level
  ) t;
END;$$;


ALTER FUNCTION "xp"."seed_default_curve"("p_app_id" "uuid", "p_max_level" integer, "p_base" numeric, "p_power" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."sync_total"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$BEGIN
  UPDATE analytics.lenser_stats e
  SET xp = NEW.total_xp,
      updated_at = now()
  WHERE e.lenser_id = NEW.lenser_id;

  RETURN NEW;
END;$$;


ALTER FUNCTION "xp"."sync_total"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."trg_ai_lenser_battle_xp_fn"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'xp', 'lensers', 'public'
    AS $$
DECLARE
  v_contender    record;
  v_owner_id     uuid;
  v_is_winner    boolean;
  v_enter_rule   uuid;
  v_win_rule     uuid;
  v_owner_enter  uuid;
  v_owner_win    uuid;
  v_app_id       uuid := '00000000-0000-0000-0000-000000000002';
BEGIN
  -- Only act on finalization (status 'closed' is the finalization status)
  IF NEW.status <> 'closed' OR OLD.status = 'closed' THEN
    RETURN NEW;
  END IF;

  -- Resolve rule IDs
  SELECT id INTO v_enter_rule FROM xp.rules
    WHERE app_id = v_app_id AND action_key = 'AI_LENSER_BATTLE_ENTERED' LIMIT 1;
  SELECT id INTO v_win_rule FROM xp.rules
    WHERE app_id = v_app_id AND action_key = 'AI_LENSER_BATTLE_WON' LIMIT 1;
  SELECT id INTO v_owner_enter FROM xp.rules
    WHERE app_id = v_app_id AND action_key = 'AI_LENSER_OWNER_BATTLE_ENTERED' LIMIT 1;
  SELECT id INTO v_owner_win FROM xp.rules
    WHERE app_id = v_app_id AND action_key = 'AI_LENSER_OWNER_BATTLE_WON' LIMIT 1;

  -- Process each AI contender
  FOR v_contender IN
    SELECT c.id, c.contender_ref_id, cem.ai_lenser_id, cem.profile_id
    FROM battles.contenders c
    JOIN battles.contender_entity_map cem ON cem.contender_id = c.id
    WHERE c.battle_id = NEW.id
      AND c.contender_type IN ('ai_model', 'ai_agent')
      AND cem.ai_lenser_id IS NOT NULL
  LOOP
    v_is_winner := (NEW.winner_contender_id = v_contender.id);

    -- Find the human owner of this AI Lenser
    SELECT o.owner_lenser_id INTO v_owner_id
    FROM agents.ownerships o
    WHERE o.ai_lenser_id = v_contender.ai_lenser_id
      AND o.role = 'owner'
      AND o.revoked_at IS NULL
    LIMIT 1;

    -- Award battle entry XP to AI Lenser's profile
    IF v_enter_rule IS NOT NULL THEN
      INSERT INTO xp.events (lenser_id, app_id, rule_id, action_key, xp, base_xp, source, source_ref_type, source_ref_id)
      SELECT
        al.profile_id, v_app_id, v_enter_rule, 'AI_LENSER_BATTLE_ENTERED',
        20 * coalesce(dm.multiplier, 1.0), 20,
        'action', 'battle', NEW.id
      FROM agents.ai_lensers al
      LEFT JOIN xp.difficulty_multipliers dm ON dm.difficulty = 'hard'
      WHERE al.id = v_contender.ai_lenser_id;
    END IF;

    -- Award battle win XP to AI Lenser
    IF v_is_winner AND v_win_rule IS NOT NULL THEN
      INSERT INTO xp.events (lenser_id, app_id, rule_id, action_key, xp, base_xp, source, source_ref_type, source_ref_id)
      SELECT
        al.profile_id, v_app_id, v_win_rule, 'AI_LENSER_BATTLE_WON',
        50 * coalesce(dm.multiplier, 1.0), 50,
        'action', 'battle', NEW.id
      FROM agents.ai_lensers al
      LEFT JOIN xp.difficulty_multipliers dm ON dm.difficulty = 'legendary'
      WHERE al.id = v_contender.ai_lenser_id;
    END IF;

    -- Award shared XP to human owner
    IF v_owner_id IS NOT NULL THEN
      IF v_owner_enter IS NOT NULL THEN
        INSERT INTO xp.events (lenser_id, app_id, rule_id, action_key, xp, base_xp, source, source_ref_type, source_ref_id)
        SELECT
          v_owner_id, v_app_id, v_owner_enter, 'AI_LENSER_OWNER_BATTLE_ENTERED',
          10 * coalesce(dm.multiplier, 1.0), 10,
          'action', 'battle', NEW.id
        FROM xp.difficulty_multipliers dm WHERE dm.difficulty = 'standard';
      END IF;

      IF v_is_winner AND v_owner_win IS NOT NULL THEN
        INSERT INTO xp.events (lenser_id, app_id, rule_id, action_key, xp, base_xp, source, source_ref_type, source_ref_id)
        SELECT
          v_owner_id, v_app_id, v_owner_win, 'AI_LENSER_OWNER_BATTLE_WON',
          25 * coalesce(dm.multiplier, 1.0), 25,
          'action', 'battle', NEW.id
        FROM xp.difficulty_multipliers dm WHERE dm.difficulty = 'hard';
      END IF;
    END IF;
  END LOOP;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "xp"."trg_ai_lenser_battle_xp_fn"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."update_daily_streak"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_streak_type" "text", "p_event_date" "date") RETURNS "void"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$
DECLARE
  v_current  integer := 0;
  v_longest  integer := 0;
  v_last     date    := NULL;
  v_new_current integer;
  v_new_longest integer;
BEGIN
  -- Fetch existing streak (may not exist)
  BEGIN
    SELECT current_streak, longest_streak, last_occurrence_at
    INTO v_current, v_longest, v_last
    FROM xp.streaks
    WHERE lenser_id  = p_lenser_id
      AND app_id     = p_app_id
      AND streak_type = p_streak_type
    FOR UPDATE;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      v_current := 0;
      v_longest := 0;
      v_last    := NULL;
  END;

  -- Calculate new streak
  IF v_last IS NULL THEN
    v_new_current := 1;
  ELSIF p_event_date = v_last THEN
    v_new_current := v_current;  -- same day, keep
  ELSIF p_event_date = v_last + 1 THEN
    v_new_current := v_current + 1;
  ELSE
    v_new_current := 1;  -- streak broken
  END IF;

  v_new_longest := GREATEST(COALESCE(v_longest, 0), v_new_current);

  -- Upsert streak record
  INSERT INTO xp.streaks (
    lenser_id, app_id, streak_type,
    current_streak, longest_streak, last_occurrence_at, updated_at
  )
  VALUES (
    p_lenser_id, p_app_id, p_streak_type,
    v_new_current, v_new_longest, p_event_date, now()
  )
  ON CONFLICT (lenser_id, app_id, streak_type)
  DO UPDATE SET
    current_streak    = EXCLUDED.current_streak,
    longest_streak    = EXCLUDED.longest_streak,
    last_occurrence_at = EXCLUDED.last_occurrence_at,
    updated_at        = EXCLUDED.updated_at;
END;
$$;


ALTER FUNCTION "xp"."update_daily_streak"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_streak_type" "text", "p_event_date" "date") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."update_season_totals"("p_app_id" "uuid", "p_lenser_id" "uuid", "p_xp" bigint) RETURNS "void"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'lensers', 'public'
    AS $$declare
  v_season_id uuid;
begin
  -- aktif sezon var mı?
  v_season_id := xp.get_active_season_id(p_app_id);

  if v_season_id is null then
    return; -- aktif sezon yoksa işlem yapma
  end if;

  -- totals güncelle
  insert into xp.season_totals (season_id, app_id, lenser_id, total_xp)
  values (v_season_id, p_app_id, p_lenser_id, p_xp)
  on conflict (season_id, app_id, lenser_id)
  do update set total_xp = xp.season_totals.total_xp + excluded.total_xp;
end;$$;


ALTER FUNCTION "xp"."update_season_totals"("p_app_id" "uuid", "p_lenser_id" "uuid", "p_xp" bigint) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "xp"."verify_signature"("p_signature" "text", "p_payload" "jsonb") RETURNS boolean
    LANGUAGE "plpgsql"
    SET "search_path" TO 'xp', 'public'
    AS $$BEGIN
  -- Temporary placeholder until signature verification is implemented.
  IF p_signature IS NULL THEN
    RETURN false;
  END IF;
  RETURN true;
END;$$;


ALTER FUNCTION "xp"."verify_signature"("p_signature" "text", "p_payload" "jsonb") OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "admin"."execution_control" (
    "id" integer DEFAULT 1 NOT NULL,
    "queue_frozen" boolean DEFAULT false NOT NULL,
    "frozen_reason" "text",
    "frozen_by" "uuid",
    "frozen_at" timestamp with time zone,
    CONSTRAINT "execution_control_id_check" CHECK (("id" = 1))
);


ALTER TABLE "admin"."execution_control" OWNER TO "postgres";


COMMENT ON TABLE "admin"."execution_control" IS 'Singleton row (id=1) tracking the platform queue-freeze toggle. queue_frozen=true halts fn_dispatch_scheduled_workflows without cancelling in-flight runs. Managed via fn_queue_freeze / fn_queue_unfreeze RPCs only.';



CREATE TABLE IF NOT EXISTS "admin"."kill_switches" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "scope" "text" NOT NULL,
    "target_id" "uuid",
    "operator_id" "uuid" NOT NULL,
    "reason" "text" NOT NULL,
    "expires_at" timestamp with time zone,
    "activated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "lifted_at" timestamp with time zone,
    CONSTRAINT "kill_switch_target_or_system" CHECK ((("scope" = 'system'::"text") OR ("target_id" IS NOT NULL))),
    CONSTRAINT "kill_switches_reason_check" CHECK (("char_length"("reason") >= 3)),
    CONSTRAINT "kill_switches_scope_check" CHECK (("scope" = ANY (ARRAY['run'::"text", 'battle'::"text", 'agent'::"text", 'system'::"text"])))
);


ALTER TABLE "admin"."kill_switches" OWNER TO "postgres";


COMMENT ON TABLE "admin"."kill_switches" IS 'Platform-wide emergency stops. scope=system halts all autonomous operations. Append-only via RPCs; never updated directly.';



CREATE TABLE IF NOT EXISTS "agents"."action_logs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "action_type" "text" NOT NULL,
    "context_ref_type" "text",
    "context_ref_id" "uuid",
    "result" "text" DEFAULT 'success'::"text" NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "action_logs_result_check" CHECK (("result" = ANY (ARRAY['success'::"text", 'blocked_by_policy'::"text", 'failed'::"text", 'throttled'::"text"]))),
    CONSTRAINT "action_logs_type_check" CHECK (("action_type" = ANY (ARRAY['join_battle'::"text", 'cast_vote'::"text", 'submit_entry'::"text", 'create_battle'::"text", 'spend_credits'::"text", 'dispatch_schedule'::"text", 'schedule_skipped'::"text", 'policy_updated'::"text", 'run_lens'::"text", 'run_workflow'::"text", 'pause_schedule'::"text", 'resume_schedule'::"text", 'binding_updated'::"text", 'standing_approval_granted'::"text", 'standing_approval_revoked'::"text", 'approval_granted_bulk'::"text"])))
);


ALTER TABLE "agents"."action_logs" OWNER TO "postgres";


COMMENT ON TABLE "agents"."action_logs" IS 'Append-only audit trail for AI Lenser autonomous actions. Created by agents.fn_agent_action only. Structural triggers prevent UPDATE and DELETE — this is a trust record, not a mutable log.';



CREATE TABLE IF NOT EXISTS "agents"."agent_run_steps" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "team_run_id" "uuid" NOT NULL,
    "team_member_id" "uuid",
    "workflow_node_id" "uuid",
    "lane" integer DEFAULT 0 NOT NULL,
    "title" "text" DEFAULT 'Task'::"text" NOT NULL,
    "current_task" "text",
    "recent_output_summary" "text",
    "blocker_summary" "text",
    "status" "text" DEFAULT 'queued'::"text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "agent_run_steps_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'running'::"text", 'completed'::"text", 'failed'::"text", 'blocked'::"text", 'skipped'::"text"])))
);


ALTER TABLE "agents"."agent_run_steps" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."workflow_assignments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "assignee_kind" "text" DEFAULT 'agent'::"text" NOT NULL,
    "assignee_ai_lenser_id" "uuid",
    "assignee_team_id" "uuid",
    "approval_policy" "jsonb" DEFAULT '{"requiresApproval": true}'::"jsonb" NOT NULL,
    "retry_policy" "jsonb" DEFAULT '{"maxRetries": 1}'::"jsonb" NOT NULL,
    "failure_policy" "jsonb" DEFAULT '{"mode": "isolate"}'::"jsonb" NOT NULL,
    "queue_policy" "jsonb" DEFAULT '{"mode": "parallel"}'::"jsonb" NOT NULL,
    "output_destination" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workflow_assignments_assignee_check" CHECK (((("assignee_kind" = 'agent'::"text") AND ("assignee_ai_lenser_id" IS NOT NULL) AND ("assignee_team_id" IS NULL)) OR (("assignee_kind" = 'team'::"text") AND ("assignee_team_id" IS NOT NULL) AND ("assignee_ai_lenser_id" IS NULL)) OR (("assignee_kind" = 'evaluator'::"text") AND ("assignee_ai_lenser_id" IS NOT NULL) AND ("assignee_team_id" IS NULL)))),
    CONSTRAINT "workflow_assignments_assignee_kind_check" CHECK (("assignee_kind" = ANY (ARRAY['agent'::"text", 'team'::"text", 'evaluator'::"text"]))),
    CONSTRAINT "workflow_assignments_target_check" CHECK (((("assignee_kind" = 'agent'::"text") AND ("assignee_ai_lenser_id" IS NOT NULL) AND ("assignee_team_id" IS NULL)) OR (("assignee_kind" = 'team'::"text") AND ("assignee_team_id" IS NOT NULL) AND ("assignee_ai_lenser_id" IS NULL))))
);


ALTER TABLE "agents"."workflow_assignments" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."workflows" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "title" "text" NOT NULL,
    "description" "text",
    "visibility" "text" DEFAULT 'public'::"text" NOT NULL,
    "battle_count" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "reaction_totals" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "fork_count" integer DEFAULT 0 NOT NULL,
    "parent_workflow_id" "uuid",
    "head_version_id" "uuid",
    "archived_at" timestamp with time zone,
    "deleted_at" timestamp with time zone,
    CONSTRAINT "ck_lenses_workflows_battle_count_nonneg" CHECK ((("battle_count" IS NULL) OR ("battle_count" >= 0))),
    CONSTRAINT "ck_lenses_workflows_fork_count_nonneg" CHECK ((("fork_count" IS NULL) OR ("fork_count" >= 0))),
    CONSTRAINT "workflows_battle_count_check" CHECK (("battle_count" >= 0)),
    CONSTRAINT "workflows_title_check" CHECK ((("char_length"(TRIM(BOTH FROM "title")) >= 1) AND ("char_length"(TRIM(BOTH FROM "title")) <= 200))),
    CONSTRAINT "workflows_visibility_check" CHECK (("visibility" = ANY (ARRAY['public'::"text", 'private'::"text", 'unlisted'::"text"])))
);


ALTER TABLE "lenses"."workflows" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflows" IS 'A Connected Lens workflow — a DAG of lens versions whose outputs feed into each other. battle_count is a denormalised counter incremented when a workflow is used in a battle.';



CREATE OR REPLACE VIEW "agents"."approval_requests_v" AS
 SELECT "tr"."id" AS "request_id",
    "tr"."ai_lenser_id",
    "tr"."team_id",
    "tr"."workflow_id",
    "tr"."workflow_assignment_id",
    "tr"."workflow_run_id",
    "tr"."status" AS "run_status",
    "tr"."approval_status",
    "tr"."metadata",
    ("tr"."metadata" ->> 'gate_kind'::"text") AS "gate_kind",
    ("tr"."metadata" ->> 'requested_action'::"text") AS "requested_action",
    ("tr"."metadata" ->> 'requester_agent_id'::"text") AS "requester_agent_id",
    "tr"."created_at" AS "requested_at",
    "tr"."started_at",
    "tr"."completed_at",
    "wa"."assignee_kind",
    "wa"."approval_policy",
    "wa"."retry_policy",
    "wa"."failure_policy",
    "w"."title" AS "workflow_title"
   FROM (("agents"."team_runs" "tr"
     LEFT JOIN "agents"."workflow_assignments" "wa" ON (("wa"."id" = "tr"."workflow_assignment_id")))
     LEFT JOIN "lenses"."workflows" "w" ON (("w"."id" = "tr"."workflow_id")))
  WHERE "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id");


ALTER VIEW "agents"."approval_requests_v" OWNER TO "postgres";


COMMENT ON VIEW "agents"."approval_requests_v" IS 'Owner-only projection of team_runs with assignment + workflow metadata for the approval queue UI. RLS-safe via agents.can_manage_ai_lenser().';



CREATE TABLE IF NOT EXISTS "agents"."battle_subscriptions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "agent_id" "uuid" NOT NULL,
    "category" "text",
    "execution_mode" "text" DEFAULT 'cloud'::"text" NOT NULL,
    "workflow_id" "uuid",
    "require_owner_approval" boolean DEFAULT false NOT NULL,
    "max_joins_per_day" integer DEFAULT 5 NOT NULL,
    "active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "battle_subscriptions_execution_mode_check" CHECK (("execution_mode" = ANY (ARRAY['local'::"text", 'cloud'::"text", 'hybrid'::"text"]))),
    CONSTRAINT "battle_subscriptions_max_joins_per_day_check" CHECK ((("max_joins_per_day" >= 1) AND ("max_joins_per_day" <= 20)))
);


ALTER TABLE "agents"."battle_subscriptions" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."evaluation_baselines" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "evaluation_id" "uuid" NOT NULL,
    "run_id" "uuid" NOT NULL,
    "score" numeric,
    "set_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "set_by" "uuid"
);


ALTER TABLE "agents"."evaluation_baselines" OWNER TO "postgres";


COMMENT ON TABLE "agents"."evaluation_baselines" IS 'Golden-run snapshot per evaluation. Used to compute regression deltas in the UI.';



CREATE TABLE IF NOT EXISTS "agents"."evaluation_case_results" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "evaluation_run_id" "uuid" NOT NULL,
    "case_id" "uuid" NOT NULL,
    "score" numeric,
    "output" "jsonb",
    "error" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "passed" boolean
);


ALTER TABLE "agents"."evaluation_case_results" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."evaluation_cases" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "evaluation_id" "uuid" NOT NULL,
    "input" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "expected" "jsonb",
    "weight" numeric DEFAULT 1 NOT NULL,
    "tags" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."evaluation_cases" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."evaluation_runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "evaluation_id" "uuid" NOT NULL,
    "model_id" "uuid",
    "status" "text" DEFAULT 'queued'::"text" NOT NULL,
    "score" numeric,
    "summary" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "completed_at" timestamp with time zone,
    "rubric_id" "uuid",
    CONSTRAINT "evaluation_runs_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'running'::"text", 'completed'::"text", 'failed'::"text", 'cancelled'::"text"])))
);


ALTER TABLE "agents"."evaluation_runs" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."evaluations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "owner_lenser_id" "uuid" NOT NULL,
    "ai_lenser_id" "uuid",
    "target_type" "text" NOT NULL,
    "target_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "scoring_rules" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "dataset_uri" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "evaluations_target_type_check" CHECK (("target_type" = ANY (ARRAY['lens'::"text", 'workflow'::"text", 'agent'::"text", 'team'::"text"])))
);


ALTER TABLE "agents"."evaluations" OWNER TO "postgres";


COMMENT ON TABLE "agents"."evaluations" IS 'Owner-defined evaluation suites for lenses, workflows, agents, or teams. Run via fn_run_evaluation.';



CREATE OR REPLACE VIEW "agents"."evaluation_results_v" AS
 SELECT "cr"."id" AS "result_id",
    "r"."id" AS "run_id",
    "r"."evaluation_id",
    "r"."status" AS "run_status",
    "r"."score" AS "run_score",
    "r"."started_at",
    "r"."completed_at",
    "c"."id" AS "case_id",
    "c"."input",
    "c"."expected",
    "c"."weight",
    "c"."tags",
    "cr"."score" AS "case_score",
    "cr"."output" AS "case_output",
    "cr"."error" AS "case_error",
    "cr"."passed"
   FROM ((("agents"."evaluation_case_results" "cr"
     JOIN "agents"."evaluation_runs" "r" ON (("r"."id" = "cr"."evaluation_run_id")))
     JOIN "agents"."evaluation_cases" "c" ON (("c"."id" = "cr"."case_id")))
     JOIN "agents"."evaluations" "e" ON (("e"."id" = "r"."evaluation_id")))
  WHERE (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id")));


ALTER VIEW "agents"."evaluation_results_v" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."evaluation_rubrics" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "evaluation_id" "uuid" NOT NULL,
    "version" integer DEFAULT 1 NOT NULL,
    "criteria" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "is_current" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."evaluation_rubrics" OWNER TO "postgres";


COMMENT ON TABLE "agents"."evaluation_rubrics" IS 'Versioned scoring criteria for an evaluation. Each call to createEvaluationRubric bumps version and marks prior rows is_current=false.';



CREATE TABLE IF NOT EXISTS "agents"."gateway_devices" (
    "device_id" "uuid" NOT NULL,
    "owner_id" "uuid" NOT NULL,
    "hostname" "text",
    "public_key" "text" NOT NULL,
    "daemon_version" "text",
    "last_seen_at" timestamp with time zone,
    "approved_at" timestamp with time zone,
    "revoked_at" timestamp with time zone,
    "kill_switch" boolean DEFAULT false NOT NULL,
    "key_rotated_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "gateway_devices_daemon_version_check" CHECK ((("daemon_version" IS NULL) OR ("char_length"("daemon_version") <= 64))),
    CONSTRAINT "gateway_devices_hostname_check" CHECK ((("hostname" IS NULL) OR ("char_length"("hostname") <= 253))),
    CONSTRAINT "gateway_devices_public_key_check" CHECK ((("char_length"("public_key") >= 32) AND ("char_length"("public_key") <= 256)))
);


ALTER TABLE "agents"."gateway_devices" OWNER TO "postgres";


COMMENT ON TABLE "agents"."gateway_devices" IS 'Phase AU: long-running gateway daemon registrations. The daemon upserts its row on every heartbeat and reacts to the {approved, kill_switch} flags returned by fn_gateway_heartbeat.';



CREATE OR REPLACE VIEW "agents"."memories_v" AS
 SELECT "m"."id",
    "m"."profile_id",
    "p"."name" AS "profile_name",
    "m"."ai_lenser_id",
    "m"."scope",
    "m"."source",
    "m"."content",
    "m"."embedding_metadata",
    "m"."confidence",
    "m"."expires_at",
    "m"."team_run_id",
    "m"."is_redacted",
    "m"."created_at"
   FROM ("agents"."memories" "m"
     JOIN "agents"."memory_profiles" "p" ON (("p"."id" = "m"."profile_id")))
  WHERE "agents"."can_manage_ai_lenser"("m"."ai_lenser_id");


ALTER VIEW "agents"."memories_v" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."memory_access_logs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "memory_id" "uuid" NOT NULL,
    "team_run_id" "uuid",
    "action" "text" NOT NULL,
    "context" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "accessed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "memory_access_logs_action_check" CHECK (("action" = ANY (ARRAY['read'::"text", 'write'::"text", 'redact'::"text"])))
);


ALTER TABLE "agents"."memory_access_logs" OWNER TO "postgres";


COMMENT ON TABLE "agents"."memory_access_logs" IS 'Audit trail for read/write/redact actions on agents.memories. One row per access.';



CREATE TABLE IF NOT EXISTS "agents"."model_profiles" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "provider_key" "text",
    "model_id" "uuid",
    "model_key" "text",
    "support_level" "text" DEFAULT 'runnable'::"text" NOT NULL,
    "params" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "is_default" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."model_profiles" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."personality_profiles" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "tone" "text" DEFAULT 'balanced'::"text" NOT NULL,
    "expertise_level" "text" DEFAULT 'specialist'::"text" NOT NULL,
    "risk_tolerance" "text" DEFAULT 'moderate'::"text" NOT NULL,
    "autonomy_level" "text" DEFAULT 'guided'::"text" NOT NULL,
    "communication_style" "text" DEFAULT 'concise'::"text" NOT NULL,
    "decision_style" "text" DEFAULT 'evidence_first'::"text" NOT NULL,
    "escalation_behavior" "text" DEFAULT 'ask_when_blocked'::"text" NOT NULL,
    "system_prompt_patch" "text" DEFAULT ''::"text" NOT NULL,
    "is_default" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."personality_profiles" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."policies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "can_join_battles" boolean DEFAULT false NOT NULL,
    "can_vote" boolean DEFAULT false NOT NULL,
    "can_create_battles" boolean DEFAULT false NOT NULL,
    "can_receive_sponsorship" boolean DEFAULT false NOT NULL,
    "model_binding_mode" "text" DEFAULT 'single'::"text" NOT NULL,
    "max_daily_battles" integer DEFAULT 0 NOT NULL,
    "max_daily_votes" integer DEFAULT 0 NOT NULL,
    "allowed_battle_types" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "spending_limit_credits" bigint DEFAULT 0 NOT NULL,
    "is_public_policy" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "allowed_output_modalities" "text"[] DEFAULT ARRAY['text'::"text"] NOT NULL,
    CONSTRAINT "policies_binding_mode_check" CHECK (("model_binding_mode" = ANY (ARRAY['single'::"text", 'multi'::"text", 'dynamic'::"text"]))),
    CONSTRAINT "policies_max_battles_nonneg" CHECK (("max_daily_battles" >= 0)),
    CONSTRAINT "policies_max_votes_nonneg" CHECK (("max_daily_votes" >= 0)),
    CONSTRAINT "policies_spending_nonneg" CHECK (("spending_limit_credits" >= 0))
);


ALTER TABLE "agents"."policies" OWNER TO "postgres";


COMMENT ON TABLE "agents"."policies" IS 'Autonomous action constraints for an AI Lenser. All behavioral flags live here — never on agents.ai_lensers. Default state is fully disabled. Owner must explicitly enable each capability after creation.';



COMMENT ON COLUMN "agents"."policies"."model_binding_mode" IS 'single = use only the default model. multi  = use any model in model_bindings. dynamic = select model per battle category via category_tags.';



COMMENT ON COLUMN "agents"."policies"."allowed_battle_types" IS 'Empty array = any battle type allowed. Non-empty = agent may only join battle_types in this list.';



COMMENT ON COLUMN "agents"."policies"."is_public_policy" IS 'When true, this agent''s policy record (capabilities, spending limits, quotas) is readable via the public read policy. Defaults to FALSE — owners must explicitly opt in to public policy transparency. Changed from original default of true in 20260326050000_agents_policy_default_hardening.';



COMMENT ON COLUMN "agents"."policies"."allowed_output_modalities" IS 'Modalities this agent is permitted to request. Defaults to [''text''] so existing agents are unaffected. Add ''image'', ''video'', ''audio'', ''music'' to unlock generative media for this agent.';



CREATE TABLE IF NOT EXISTS "agents"."policy_evaluations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "team_run_id" "uuid",
    "tool_invocation_id" "uuid",
    "evaluation_point" "text" NOT NULL,
    "policy_type" "text" NOT NULL,
    "verdict" "text" NOT NULL,
    "reason" "text",
    "context" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "evaluated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "policy_evaluations_evaluation_point_check" CHECK (("evaluation_point" = ANY (ARRAY['pre_run'::"text", 'pre_side_effect'::"text", 'pre_tool'::"text"]))),
    CONSTRAINT "policy_evaluations_policy_type_check" CHECK (("policy_type" = ANY (ARRAY['budget'::"text", 'kill_switch'::"text", 'pause'::"text", 'parallel_limit'::"text", 'dark_launch'::"text", 'approval'::"text"]))),
    CONSTRAINT "policy_evaluations_verdict_check" CHECK (("verdict" = ANY (ARRAY['allow'::"text", 'deny'::"text", 'pause'::"text", 'require_approval'::"text"])))
);


ALTER TABLE "agents"."policy_evaluations" OWNER TO "postgres";


COMMENT ON TABLE "agents"."policy_evaluations" IS 'Immutable audit log of pre-run, pre-side-effect, and pre-tool policy verdicts. One row per evaluation point; never updated after insert.';



CREATE TABLE IF NOT EXISTS "agents"."quota_snapshots" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "period_date" "date" DEFAULT CURRENT_DATE NOT NULL,
    "battles_used" integer DEFAULT 0 NOT NULL,
    "votes_used" integer DEFAULT 0 NOT NULL,
    "credits_spent" bigint DEFAULT 0 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "quota_battles_nonneg" CHECK (("battles_used" >= 0)),
    CONSTRAINT "quota_credits_nonneg" CHECK (("credits_spent" >= 0)),
    CONSTRAINT "quota_votes_nonneg" CHECK (("votes_used" >= 0))
);


ALTER TABLE "agents"."quota_snapshots" OWNER TO "postgres";


COMMENT ON TABLE "agents"."quota_snapshots" IS 'Daily usage counters per AI Lenser. Upserted atomically by agents.fn_agent_action. Avoids scanning action_logs for quota checks. One row per (ai_lenser_id, date). Row is lazily created on first action.';



CREATE TABLE IF NOT EXISTS "agents"."run_incidents" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_report_id" "uuid" NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "incident_type" "text" NOT NULL,
    "severity" "text" NOT NULL,
    "title" "text" NOT NULL,
    "description" "text",
    "context" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "resolved_at" timestamp with time zone,
    "resolution" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "run_incidents_incident_type_check" CHECK (("incident_type" = ANY (ARRAY['tool_failure'::"text", 'budget_exceeded'::"text", 'policy_violation'::"text", 'timeout'::"text", 'step_failure'::"text", 'approval_rejected'::"text", 'kill_switch'::"text"]))),
    CONSTRAINT "run_incidents_severity_check" CHECK (("severity" = ANY (ARRAY['low'::"text", 'medium'::"text", 'high'::"text", 'critical'::"text"])))
);


ALTER TABLE "agents"."run_incidents" OWNER TO "postgres";


COMMENT ON TABLE "agents"."run_incidents" IS 'Incident log attached to a run report. Records tool failures, budget overruns, policy violations, timeouts, and kill-switch events with severity classification.';



CREATE TABLE IF NOT EXISTS "agents"."run_reports" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "team_run_id" "uuid",
    "workflow_run_id" "uuid",
    "title" "text" NOT NULL,
    "summary" "text",
    "metrics" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "total_steps" integer DEFAULT 0 NOT NULL,
    "total_tool_invocations" integer DEFAULT 0 NOT NULL,
    "total_memory_writes" integer DEFAULT 0 NOT NULL,
    "total_cost_estimate" numeric DEFAULT 0 NOT NULL,
    "evaluation_score" numeric,
    "outcome" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_agents_run_reports_evaluation_score_nonneg" CHECK ((("evaluation_score" IS NULL) OR ("evaluation_score" >= (0)::numeric))),
    CONSTRAINT "ck_agents_run_reports_total_cost_estimate_nonneg" CHECK ((("total_cost_estimate" IS NULL) OR ("total_cost_estimate" >= (0)::numeric))),
    CONSTRAINT "ck_agents_run_reports_total_memory_writes_nonneg" CHECK ((("total_memory_writes" IS NULL) OR ("total_memory_writes" >= 0))),
    CONSTRAINT "ck_agents_run_reports_total_steps_nonneg" CHECK ((("total_steps" IS NULL) OR ("total_steps" >= 0))),
    CONSTRAINT "ck_agents_run_reports_total_tool_invocations_nonneg" CHECK ((("total_tool_invocations" IS NULL) OR ("total_tool_invocations" >= 0))),
    CONSTRAINT "run_reports_outcome_check" CHECK (("outcome" = ANY (ARRAY['success'::"text", 'partial'::"text", 'failed'::"text", 'cancelled'::"text", 'killed'::"text"]))),
    CONSTRAINT "run_reports_total_cost_estimate_check" CHECK (("total_cost_estimate" >= (0)::numeric)),
    CONSTRAINT "run_reports_total_memory_writes_check" CHECK (("total_memory_writes" >= 0)),
    CONSTRAINT "run_reports_total_steps_check" CHECK (("total_steps" >= 0)),
    CONSTRAINT "run_reports_total_tool_invocations_check" CHECK (("total_tool_invocations" >= 0))
);


ALTER TABLE "agents"."run_reports" OWNER TO "postgres";


COMMENT ON TABLE "agents"."run_reports" IS 'Immutable post-run summary records. One report per terminal team run. Aggregates steps, tool invocations, memory writes, cost, and evaluation score at the moment of report creation.';



CREATE TABLE IF NOT EXISTS "agents"."standing_approvals" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "workflow_id" "uuid",
    "gate_kind" "text",
    "expires_at" timestamp with time zone,
    "created_by" "uuid" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "revoked_at" timestamp with time zone,
    CONSTRAINT "standing_approvals_target_required" CHECK ((("workflow_id" IS NOT NULL) OR ("gate_kind" IS NOT NULL)))
);


ALTER TABLE "agents"."standing_approvals" OWNER TO "postgres";


COMMENT ON TABLE "agents"."standing_approvals" IS 'Phase Q2: pre-granted approvals for AI lenser workflow dispatches. A row matches when ai_lenser_id matches and (workflow_id or gate_kind) aligns. NULL expires_at means the approval never expires until revoked.';



CREATE TABLE IF NOT EXISTS "agents"."team_edges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "team_id" "uuid" NOT NULL,
    "source_member_id" "uuid" NOT NULL,
    "target_member_id" "uuid" NOT NULL,
    "edge_type" "text" DEFAULT 'delegates'::"text" NOT NULL,
    "is_blocking" boolean DEFAULT false NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "team_edges_edge_type_check" CHECK (("edge_type" = ANY (ARRAY['delegates'::"text", 'reviews'::"text", 'reports_to'::"text", 'shares_context'::"text", 'handoff'::"text"]))),
    CONSTRAINT "team_edges_no_self_loop" CHECK (("source_member_id" <> "target_member_id"))
);


ALTER TABLE "agents"."team_edges" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."team_members" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "team_id" "uuid" NOT NULL,
    "agent_id" "uuid" NOT NULL,
    "role" "text" DEFAULT 'operator'::"text" NOT NULL,
    "responsibility" "text" DEFAULT ''::"text" NOT NULL,
    "lane" integer DEFAULT 0 NOT NULL,
    "personality_profile_id" "uuid",
    "memory_profile_id" "uuid",
    "tool_profile_id" "uuid",
    "model_profile_id" "uuid",
    "sort_order" integer DEFAULT 0 NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "team_members_role_check" CHECK (("role" = ANY (ARRAY['leader'::"text", 'executor'::"text", 'reviewer'::"text", 'observer'::"text", 'operator'::"text"])))
);


ALTER TABLE "agents"."team_members" OWNER TO "postgres";


COMMENT ON CONSTRAINT "team_members_role_check" ON "agents"."team_members" IS 'Phase X4: restricted role enum. Pre-existing rows with non-conforming roles must be backfilled before this CHECK can be re-validated; new inserts are constrained immediately. Operators outside the enum are silently bypassed by fn_node_requires_review and similar gates.';



CREATE TABLE IF NOT EXISTS "agents"."team_messages" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "team_run_id" "uuid" NOT NULL,
    "from_agent_id" "uuid" NOT NULL,
    "to_agent_id" "uuid",
    "kind" "text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "parent_message_id" "uuid",
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "team_messages_kind_check" CHECK (("kind" = ANY (ARRAY['task_request'::"text", 'task_response'::"text", 'info'::"text", 'error'::"text"])))
);


ALTER TABLE "agents"."team_messages" OWNER TO "postgres";


COMMENT ON TABLE "agents"."team_messages" IS 'Phase X1 inter-agent message bus. Append-only log scoped to a team_run. NULL to_agent_id == broadcast to all members of the team_run. Hard cap 1000 rows per team_run enforced by BEFORE INSERT trigger.';



CREATE TABLE IF NOT EXISTS "agents"."teams" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    "scratchpad" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "teams_name_length_check" CHECK ((("char_length"(TRIM(BOTH FROM "name")) >= 1) AND ("char_length"(TRIM(BOTH FROM "name")) <= 120))),
    CONSTRAINT "teams_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'paused'::"text", 'archived'::"text"])))
);


ALTER TABLE "agents"."teams" OWNER TO "postgres";


COMMENT ON TABLE "agents"."teams" IS 'Owner-managed agent teams for an AI workspace control room.';



CREATE TABLE IF NOT EXISTS "agents"."tool_invocations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "team_run_id" "uuid" NOT NULL,
    "agent_run_step_id" "uuid",
    "tool_id" "uuid" NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "input" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "output" "jsonb",
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "approval_status" "text" DEFAULT 'not_required'::"text" NOT NULL,
    "approval_required" boolean DEFAULT false NOT NULL,
    "approval_decided_by" "uuid",
    "approval_reason" "text",
    "error" "text",
    "cost_estimate" numeric,
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tool_invocations_approval_status_check" CHECK (("approval_status" = ANY (ARRAY['pending'::"text", 'approved'::"text", 'rejected'::"text", 'not_required'::"text"]))),
    CONSTRAINT "tool_invocations_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'approved'::"text", 'rejected'::"text", 'running'::"text", 'completed'::"text", 'failed'::"text"])))
);


ALTER TABLE "agents"."tool_invocations" OWNER TO "postgres";


COMMENT ON TABLE "agents"."tool_invocations" IS 'Per-call log of tool executions during agent runs. Carries approval state, I/O, cost, and timing.';



CREATE OR REPLACE VIEW "agents"."tool_invocations_v" AS
 SELECT "inv"."id",
    "inv"."team_run_id",
    "inv"."agent_run_step_id",
    "inv"."tool_id",
    "inv"."ai_lenser_id",
    "tr"."key" AS "tool_key",
    "tr"."name" AS "tool_name",
    "tr"."category" AS "tool_category",
    "tr"."egress_class",
    "tr"."is_dangerous",
    "step"."title" AS "step_title",
    "inv"."input",
    "inv"."output",
    "inv"."status",
    "inv"."approval_status",
    "inv"."approval_required",
    "inv"."approval_decided_by",
    "inv"."approval_reason",
    "inv"."error",
    "inv"."cost_estimate",
    "inv"."started_at",
    "inv"."completed_at",
    "inv"."created_at"
   FROM (("agents"."tool_invocations" "inv"
     JOIN "agents"."tools_registry" "tr" ON (("tr"."id" = "inv"."tool_id")))
     LEFT JOIN "agents"."agent_run_steps" "step" ON (("step"."id" = "inv"."agent_run_step_id")))
  WHERE "agents"."can_manage_ai_lenser"("inv"."ai_lenser_id");


ALTER VIEW "agents"."tool_invocations_v" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "agents"."tool_profiles" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "allow_tools" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "deny_tools" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "tool_groups" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "provider_overrides" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "requires_approval" boolean DEFAULT true NOT NULL,
    "is_default" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."tool_profiles" OWNER TO "postgres";


CREATE OR REPLACE VIEW "agents"."v_agent_profile" AS
 SELECT "a"."id",
    "a"."id" AS "ai_lenser_id",
    "a"."profile_id",
    "p"."handle",
    "p"."display_name",
    "p"."avatar_url",
    "p"."type" AS "lenser_type",
    "a"."runtime_pref",
    "a"."is_active",
    "a"."suspended_at",
    "a"."suspended_reason",
    "a"."personality_note",
    "a"."created_at",
    "pol"."can_join_battles",
    "pol"."can_vote",
    "pol"."can_create_battles",
    "pol"."can_receive_sponsorship",
    "pol"."model_binding_mode",
    "pol"."max_daily_battles",
    "pol"."max_daily_votes",
    "pol"."spending_limit_credits",
    "pol"."allowed_battle_types",
    "pol"."is_public_policy",
    ( SELECT "count"(*) AS "count"
           FROM "agents"."model_bindings" "mb"
          WHERE ("mb"."ai_lenser_id" = "a"."id")) AS "model_count",
    ( SELECT "count"(*) AS "count"
           FROM "agents"."lens_bindings" "lb"
          WHERE ("lb"."ai_lenser_id" = "a"."id")) AS "lens_count",
    COALESCE("qs"."battles_used", 0) AS "battles_used",
    COALESCE("qs"."votes_used", 0) AS "votes_used",
    COALESCE("qs"."credits_spent", (0)::bigint) AS "credits_spent",
    "own"."owner_lenser_id",
    "op"."handle" AS "owner_handle",
    "op"."display_name" AS "owner_display_name",
    "op"."avatar_url" AS "owner_avatar_url",
    COALESCE(( SELECT "count"(*) AS "count"
           FROM ("battles"."contenders" "c"
             JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
          WHERE (("c"."contender_ref_id" = "a"."id") AND ("b"."status" = ANY (ARRAY['published'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum"])))), (0)::bigint) AS "total_battles",
    COALESCE(( SELECT "count"(*) AS "count"
           FROM ("battles"."contenders" "c"
             JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
          WHERE (("c"."contender_ref_id" = "a"."id") AND ("b"."winner_contender_id" = "c"."id"))), (0)::bigint) AS "battles_won",
    COALESCE(( SELECT "count"(*) AS "count"
           FROM ("battles"."contenders" "c"
             JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
          WHERE (("c"."contender_ref_id" = "a"."id") AND ("b"."status" = ANY (ARRAY['published'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum"])) AND ("b"."winner_contender_id" IS NOT NULL) AND ("b"."winner_contender_id" <> "c"."id"))), (0)::bigint) AS "battles_lost",
    "round"((((COALESCE(( SELECT "count"(*) AS "count"
           FROM ("battles"."contenders" "c"
             JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
          WHERE (("c"."contender_ref_id" = "a"."id") AND ("b"."winner_contender_id" = "c"."id"))), (0)::bigint))::numeric * 100.0) / (NULLIF(COALESCE(( SELECT "count"(*) AS "count"
           FROM ("battles"."contenders" "c"
             JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
          WHERE (("c"."contender_ref_id" = "a"."id") AND ("b"."status" = ANY (ARRAY['published'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum"])))), (0)::bigint), 0))::numeric), 1) AS "win_rate"
   FROM ((((("agents"."ai_lensers" "a"
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "a"."profile_id")))
     LEFT JOIN "agents"."policies" "pol" ON (("pol"."ai_lenser_id" = "a"."id")))
     LEFT JOIN "agents"."quota_snapshots" "qs" ON ((("qs"."ai_lenser_id" = "a"."id") AND ("qs"."period_date" = CURRENT_DATE))))
     LEFT JOIN "agents"."ownerships" "own" ON ((("own"."ai_lenser_id" = "a"."id") AND ("own"."role" = 'owner'::"text") AND ("own"."revoked_at" IS NULL))))
     LEFT JOIN "lensers"."profiles" "op" ON (("op"."id" = "own"."owner_lenser_id")));


ALTER VIEW "agents"."v_agent_profile" OWNER TO "postgres";


COMMENT ON VIEW "agents"."v_agent_profile" IS 'Full AI Lenser management profile. Includes personality_note, battle stats, policy, quota, and ownership fields. Uses owner join to enable human-owner auth checks in SECURITY DEFINER RPCs.';



CREATE OR REPLACE VIEW "agents"."v_gateway_device_health" AS
 SELECT "d"."device_id",
    "d"."owner_id",
    "d"."hostname",
    "d"."daemon_version",
    "d"."last_seen_at",
    "d"."approved_at",
    "d"."revoked_at",
    "d"."kill_switch",
    "d"."created_at",
    COALESCE("c"."pending_commands", (0)::bigint) AS "pending_commands",
    COALESCE("c"."unacked_commands", (0)::bigint) AS "unacked_commands"
   FROM ("agents"."gateway_devices" "d"
     LEFT JOIN LATERAL ( SELECT "count"(*) FILTER (WHERE ("gc"."claimed_at" IS NULL)) AS "pending_commands",
            "count"(*) FILTER (WHERE ("gc"."acked_at" IS NULL)) AS "unacked_commands"
           FROM "agents"."gateway_commands" "gc"
          WHERE ("gc"."device_id" = "d"."device_id")) "c" ON (true));


ALTER VIEW "agents"."v_gateway_device_health" OWNER TO "postgres";


COMMENT ON VIEW "agents"."v_gateway_device_health" IS 'Phase BE: per-device daemon health snapshot (heartbeat + outbox depth).';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_schedules" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "cron_expr" "text" NOT NULL,
    "global_model_id" "text",
    "inputs_template" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "last_run_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "last_run_id" "uuid",
    "last_dispatch_status" "text",
    "last_error_at" timestamp with time zone,
    "last_error_message" "text",
    "timezone" "text" DEFAULT 'UTC'::"text" NOT NULL,
    "next_run_at" timestamp with time zone,
    "assignee_type" "text" DEFAULT 'agent'::"text" NOT NULL,
    "assignee_id" "uuid",
    "workflow_assignment_id" "uuid",
    "approval_policy" "jsonb" DEFAULT '{"requiresApproval": true}'::"jsonb" NOT NULL,
    "retry_policy" "jsonb" DEFAULT '{"maxRetries": 1}'::"jsonb" NOT NULL,
    "failure_policy" "jsonb" DEFAULT '{"mode": "isolate"}'::"jsonb" NOT NULL,
    "queue_policy" "jsonb" DEFAULT '{"mode": "parallel"}'::"jsonb" NOT NULL,
    "last_completed_at" timestamp with time zone,
    "last_result" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "description" "text",
    "calendar_id" "uuid",
    "pre_dispatch_condition" "jsonb",
    "inputs_rotation" "jsonb",
    "last_rotation_index" integer DEFAULT 0 NOT NULL,
    CONSTRAINT "workflow_schedules_assignee_type_check" CHECK (("assignee_type" = ANY (ARRAY['agent'::"text", 'team'::"text"])))
);


ALTER TABLE "lenses"."workflow_schedules" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_schedules" IS 'CRON schedules for automated workflow runs. RLS: SELECT is scoped to owning lenser; writes must go through fn_upsert_workflow_schedule.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."last_run_id" IS 'Most recent workflow_runs.id dispatched by this schedule.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."last_dispatch_status" IS 'Best-effort scheduler status: dispatched, skipped_overlap, validation_failed, or dispatch_failed.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."last_error_at" IS 'Timestamp of the most recent schedule dispatch failure or validation skip.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."last_error_message" IS 'Human-readable reason for the most recent schedule dispatch failure/skip.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."timezone" IS 'IANA timezone used for CRON evaluation and next-run display.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."assignee_type" IS 'Whether this schedule targets a single agent or an agent team.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."assignee_id" IS 'Polymorphic target identifier. Resolves to agents.ai_lensers.id for agent schedules or agents.teams.id for team schedules.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."workflow_assignment_id" IS 'Optional explicit workflow assignment backing this schedule.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."calendar_id" IS 'Phase W1: optional calendar overlay (skip_dates / only_dates). NULL = no overlay. ON DELETE SET NULL so deleting a calendar does not cascade-delete schedules.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."pre_dispatch_condition" IS 'Phase W2: optional jsonb condition evaluated by automation.fn_eval_filter against lenses.fn_build_schedule_condition_context(...). NULL = no condition. Same DSL as Phase U trigger rules (JSON-Pointer + eq/neq/gt/lt/contains).';



COMMENT ON COLUMN "lenses"."workflow_schedules"."inputs_rotation" IS 'Phase W3: optional jsonb array of input templates. When non-empty, the dispatcher rotates through entries using last_rotation_index. NULL or empty = use inputs_template.';



COMMENT ON COLUMN "lenses"."workflow_schedules"."last_rotation_index" IS 'Phase W3: monotonic counter incremented each tick the rotation array is used. The selected entry index is (last_rotation_index % array_length). Reset to 0 by the operator if the rotation array shape changes.';



CREATE OR REPLACE VIEW "agents"."v_human_fleet_overview" AS
 SELECT "o"."owner_lenser_id" AS "human_lenser_id",
    "count"(DISTINCT "al"."id") AS "agents_total",
    "count"(DISTINCT "al"."id") FILTER (WHERE ("al"."is_active" AND ("al"."suspended_at" IS NULL))) AS "agents_active",
    COALESCE("sum"("qs"."credits_spent"), (0)::numeric) AS "credits_30d",
    "count"(DISTINCT "tr"."id") FILTER (WHERE ("tr"."created_at" >= ("now"() - '24:00:00'::interval))) AS "runs_24h",
    "count"(DISTINCT "tr"."id") FILTER (WHERE ("tr"."approval_status" = 'pending'::"text")) AS "approvals_pending",
    "count"(DISTINCT "ws"."id") FILTER (WHERE "ws"."is_active") AS "schedules_active"
   FROM (((("agents"."ownerships" "o"
     JOIN "agents"."ai_lensers" "al" ON (("al"."id" = "o"."ai_lenser_id")))
     LEFT JOIN "agents"."team_runs" "tr" ON (("tr"."ai_lenser_id" = "al"."id")))
     LEFT JOIN "agents"."quota_snapshots" "qs" ON ((("qs"."ai_lenser_id" = "al"."id") AND ("qs"."period_date" >= (CURRENT_DATE - '30 days'::interval)))))
     LEFT JOIN "lenses"."workflow_schedules" "ws" ON (((("ws"."assignee_type" = 'agent'::"text") AND ("ws"."assignee_id" = "al"."id")) OR (("ws"."assignee_type" = 'team'::"text") AND ("ws"."assignee_id" IN ( SELECT "t"."id"
           FROM "agents"."teams" "t"
          WHERE ("t"."ai_lenser_id" = "al"."id")))))))
  GROUP BY "o"."owner_lenser_id";


ALTER VIEW "agents"."v_human_fleet_overview" OWNER TO "postgres";


COMMENT ON VIEW "agents"."v_human_fleet_overview" IS 'Per-human-owner fleet aggregations across owned agents. Used by the human-mode Overview section.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "triggered_by" "uuid",
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "context_inputs" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "global_model_id" "text",
    "budget_credits" integer,
    "spent_credits" integer DEFAULT 0 NOT NULL,
    "cost_metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "idempotency_key" "text",
    "heartbeat_at" timestamp with time zone,
    "run_worker_id" "text",
    "parent_run_id" "uuid",
    "recursion_depth" integer DEFAULT 0 NOT NULL,
    "schedule_id" "uuid",
    "trigger_mode" "text" DEFAULT 'manual'::"text" NOT NULL,
    "active_node_id" "uuid",
    "scheduled_for" timestamp with time zone,
    "ai_lenser_id" "uuid",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "media_manifest" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "idempotency_expires_at" timestamp with time zone,
    "workflow_version_id" "uuid",
    "workflow_snapshot_hash" "text",
    "redacted_agent_snapshot" "jsonb",
    "redacted_agent_snapshot_hash" "text",
    CONSTRAINT "wf_runs_budget_nonneg" CHECK ((("budget_credits" IS NULL) OR ("budget_credits" >= 0))),
    CONSTRAINT "wf_runs_recursion_depth_cap" CHECK ((("recursion_depth" >= 0) AND ("recursion_depth" <= 8))),
    CONSTRAINT "wf_runs_spent_nonneg" CHECK (("spent_credits" >= 0)),
    CONSTRAINT "wf_runs_timestamp_order" CHECK ((("completed_at" IS NULL) OR ("started_at" IS NULL) OR ("completed_at" >= "started_at"))),
    CONSTRAINT "workflow_runs_status_check" CHECK (("status" = ANY (ARRAY['draft'::"text", 'validated'::"text", 'queued'::"text", 'pending'::"text", 'running'::"text", 'streaming'::"text", 'recovered'::"text", 'completed'::"text", 'failed'::"text", 'cancelled'::"text", 'timed_out'::"text"]))),
    CONSTRAINT "workflow_runs_trigger_mode_check" CHECK (("trigger_mode" = ANY (ARRAY['manual'::"text", 'schedule'::"text", 'subflow'::"text"])))
);


ALTER TABLE "lenses"."workflow_runs" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_runs" IS 'A single execution of a workflow. The CF Worker drives status transitions. context_inputs holds root-level input values for nodes with no incoming edges.';



COMMENT ON COLUMN "lenses"."workflow_runs"."global_model_id" IS 'Default AI model for this run. Individual nodes can override via workflow_nodes.config.model_id.';



COMMENT ON COLUMN "lenses"."workflow_runs"."budget_credits" IS 'Maximum credits allowed for this run. NULL = unlimited.';



COMMENT ON COLUMN "lenses"."workflow_runs"."spent_credits" IS 'Total credits spent so far across all completed nodes.';



COMMENT ON COLUMN "lenses"."workflow_runs"."cost_metadata" IS 'Per-node cost breakdown: { node_id: { input_tokens, output_tokens, cost_credits } }';



COMMENT ON COLUMN "lenses"."workflow_runs"."idempotency_key" IS 'Client-derived sha256(workflow_id || rootInputsCanonicalJson). Prevents double-trigger when the UI retries a submit.';



COMMENT ON COLUMN "lenses"."workflow_runs"."heartbeat_at" IS 'Last heartbeat from the worker currently owning this run. NULL until the first wave begins. Used by the crash-recovery loop (§6.8) to detect stale runs.';



COMMENT ON COLUMN "lenses"."workflow_runs"."run_worker_id" IS 'Opaque identifier for the worker process that currently owns this run. Set by fn_claim_stale_workflow_run; cleared on terminal statuses.';



COMMENT ON COLUMN "lenses"."workflow_runs"."parent_run_id" IS 'Parent workflow_runs.id when this run was spawned by an orchestration lens subflow. NULL for top-level runs.';



COMMENT ON COLUMN "lenses"."workflow_runs"."recursion_depth" IS 'Depth of this run in the orchestration lens chain (0 = top-level). Capped at 8 to prevent runaway subflows (Phase 9 §Orchestration is bounded).';



COMMENT ON COLUMN "lenses"."workflow_runs"."schedule_id" IS 'Owning workflow_schedules.id when the run was dispatched by the scheduler. NULL for manual runs.';



COMMENT ON COLUMN "lenses"."workflow_runs"."trigger_mode" IS 'How the workflow run was triggered: manual, schedule, or subflow.';



COMMENT ON COLUMN "lenses"."workflow_runs"."active_node_id" IS 'The node currently executing for this run (or the most recently active one). Written by fn_update_workflow_node_result whenever a node enters running/streaming/retrying. NULL on terminal runs.';



COMMENT ON COLUMN "lenses"."workflow_runs"."scheduled_for" IS 'Normalized schedule slot (minute precision) that produced this run. NULL for manual or subflow runs.';



COMMENT ON COLUMN "lenses"."workflow_runs"."ai_lenser_id" IS 'The AI lenser acting as the run executor. NULL when the run is created directly by the
   human workspace. Populated by scheduled dispatch and by agent-context executions.';



COMMENT ON COLUMN "lenses"."workflow_runs"."metadata" IS 'Free-form per-run metadata. Reserved keys: backfill_id (for Phase Q1 replay dedup) and origin (e.g. ''backfill'', ''manual'').';



COMMENT ON COLUMN "lenses"."workflow_runs"."media_manifest" IS 'AP: Ordered list of media produced by this run. Each entry: {object_id, media_type, mime_type, node_id, added_at}. Populated by fn_append_workflow_run_media via WorkflowExecutionService.';



COMMENT ON COLUMN "lenses"."workflow_runs"."idempotency_expires_at" IS 'D4: idempotency window. After this time the (workflow_id, idempotency_key) lookup ignores this row, allowing a fresh run with the same key.';



COMMENT ON COLUMN "lenses"."workflow_runs"."workflow_version_id" IS 'Immutable workflow version snapshot bound at run start.';



COMMENT ON COLUMN "lenses"."workflow_runs"."workflow_snapshot_hash" IS 'SHA-256 hash of the workflow DAG snapshot used by this run.';



COMMENT ON CONSTRAINT "workflow_runs_status_check" ON "lenses"."workflow_runs" IS 'Aligns with the engine state machine (Phase 1 §5). `recovered` is a transient status written by the crash-recovery loop before the run resumes. Terminal states: completed, failed, cancelled, timed_out.';



CREATE OR REPLACE VIEW "agents"."v_run_unified" AS
 SELECT "tr"."id" AS "run_id",
    'team'::"text" AS "run_type",
    "tr"."ai_lenser_id",
    "tr"."status",
    "tr"."approval_status",
    COALESCE("ti_agg"."total_cost", (0)::numeric) AS "total_cost",
    COALESCE("step_agg"."step_count", (0)::bigint) AS "step_count",
    COALESCE("mem_agg"."memory_write_count", (0)::bigint) AS "memory_write_count",
    "eval_agg"."latest_score" AS "latest_evaluation_score",
    "tr"."started_at",
    "tr"."completed_at",
        CASE
            WHEN (("tr"."started_at" IS NOT NULL) AND ("tr"."completed_at" IS NOT NULL)) THEN EXTRACT(epoch FROM ("tr"."completed_at" - "tr"."started_at"))
            ELSE NULL::numeric
        END AS "duration_seconds"
   FROM (((("agents"."team_runs" "tr"
     LEFT JOIN ( SELECT "tool_invocations"."team_run_id",
            "sum"("tool_invocations"."cost_estimate") AS "total_cost"
           FROM "agents"."tool_invocations"
          GROUP BY "tool_invocations"."team_run_id") "ti_agg" ON (("ti_agg"."team_run_id" = "tr"."id")))
     LEFT JOIN ( SELECT "agent_run_steps"."team_run_id",
            "count"(*) AS "step_count"
           FROM "agents"."agent_run_steps"
          GROUP BY "agent_run_steps"."team_run_id") "step_agg" ON (("step_agg"."team_run_id" = "tr"."id")))
     LEFT JOIN ( SELECT "memories"."team_run_id",
            "count"(*) AS "memory_write_count"
           FROM "agents"."memories"
          WHERE ("memories"."source" = ANY (ARRAY['agent'::"text", 'tool'::"text"]))
          GROUP BY "memories"."team_run_id") "mem_agg" ON (("mem_agg"."team_run_id" = "tr"."id")))
     LEFT JOIN LATERAL ( SELECT "er"."score" AS "latest_score"
           FROM ("agents"."evaluation_runs" "er"
             JOIN "agents"."evaluations" "e" ON (("e"."id" = "er"."evaluation_id")))
          WHERE (("e"."target_type" = 'workflow'::"text") AND ("e"."target_id" = "tr"."workflow_id") AND ("er"."status" = 'completed'::"text"))
          ORDER BY "er"."completed_at" DESC NULLS LAST
         LIMIT 1) "eval_agg" ON (true))
  WHERE (("tr"."workflow_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id"))
UNION ALL
 SELECT "wr"."id" AS "run_id",
    'workflow'::"text" AS "run_type",
    "wr"."ai_lenser_id",
    "wr"."status",
    NULL::"text" AS "approval_status",
    COALESCE("wr"."spent_credits", 0) AS "total_cost",
    0 AS "step_count",
    0 AS "memory_write_count",
    NULL::numeric AS "latest_evaluation_score",
    "wr"."started_at",
    "wr"."completed_at",
        CASE
            WHEN (("wr"."started_at" IS NOT NULL) AND ("wr"."completed_at" IS NOT NULL)) THEN EXTRACT(epoch FROM ("wr"."completed_at" - "wr"."started_at"))
            ELSE NULL::numeric
        END AS "duration_seconds"
   FROM "lenses"."workflow_runs" "wr"
  WHERE (("wr"."ai_lenser_id" IS NOT NULL) AND (NOT (EXISTS ( SELECT 1
           FROM "agents"."team_runs" "tr2"
          WHERE ("tr2"."workflow_run_id" = "wr"."id")))) AND "agents"."can_manage_ai_lenser"("wr"."ai_lenser_id"));


ALTER VIEW "agents"."v_run_unified" OWNER TO "postgres";


CREATE OR REPLACE VIEW "agents"."v_team_run_conversation" WITH ("security_invoker"='on') AS
 WITH RECURSIVE "thread" AS (
         SELECT "m"."team_run_id",
            "m"."id" AS "message_id",
            "m"."parent_message_id",
            "m"."from_agent_id",
            "m"."to_agent_id",
            "m"."kind",
            "m"."payload",
            "m"."occurred_at",
            0 AS "depth"
           FROM "agents"."team_messages" "m"
          WHERE ("m"."parent_message_id" IS NULL)
        UNION ALL
         SELECT "m"."team_run_id",
            "m"."id",
            "m"."parent_message_id",
            "m"."from_agent_id",
            "m"."to_agent_id",
            "m"."kind",
            "m"."payload",
            "m"."occurred_at",
            ("t"."depth" + 1)
           FROM ("agents"."team_messages" "m"
             JOIN "thread" "t" ON ((("m"."parent_message_id" = "t"."message_id") AND ("m"."team_run_id" = "t"."team_run_id"))))
          WHERE ("t"."depth" < 50)
        )
 SELECT "team_run_id",
    "message_id",
    "parent_message_id",
    "from_agent_id",
    "to_agent_id",
    "kind",
    "payload",
    "occurred_at",
    "depth"
   FROM "thread";


ALTER VIEW "agents"."v_team_run_conversation" OWNER TO "postgres";


COMMENT ON VIEW "agents"."v_team_run_conversation" IS 'Phase X1 (hardened): security_invoker=on. SELECT through this view is evaluated under the caller''s RLS context against agents.team_messages.';



CREATE TABLE IF NOT EXISTS "agents"."workspace_switches" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "human_lenser_id" "uuid" NOT NULL,
    "from_ai_lenser_id" "uuid",
    "to_ai_lenser_id" "uuid",
    "switched_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "agents"."workspace_switches" OWNER TO "postgres";


COMMENT ON TABLE "agents"."workspace_switches" IS 'Append-only audit trail of workspace (profile) switches. from_ai_lenser_id NULL means
   the user was on their human workspace; to_ai_lenser_id NULL means they returned to it.';



CREATE TABLE IF NOT EXISTS "ai"."key_usage_log" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "key_id" "uuid" NOT NULL,
    "run_id" "uuid",
    "lenser_id" "uuid" NOT NULL,
    "workspace_id" "uuid",
    "token_input" integer,
    "token_output" integer,
    "credit_cost" bigint,
    "provider" "text",
    "model_key" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "ai"."key_usage_log" OWNER TO "postgres";


COMMENT ON TABLE "ai"."key_usage_log" IS 'Immutable BYOK key usage audit. One row per completed run that used a BYOK key. Written exclusively by cloud worker (service_role) after run completion. Never updated or deleted by authenticated users. Enables per-key billing aggregation, abuse detection, and key rotation safety checks (find all runs using a key before revoking it).';



COMMENT ON COLUMN "ai"."key_usage_log"."key_id" IS 'The BYOK key used for this run. ON DELETE RESTRICT prevents key deletion while usage records exist — rotate keys via revoke (ai.keys.status=revoked), not hard-delete.';



COMMENT ON COLUMN "ai"."key_usage_log"."run_id" IS 'The execution run that consumed the key. SET NULL on run deletion (soft-deleted runs set is_active=false; hard deletion is exceptional). NULL indicates the run record was removed after the log was written.';



COMMENT ON COLUMN "ai"."key_usage_log"."workspace_id" IS 'Tenant context at the time of execution. Enables per-workspace BYOK usage reports. NULL for legacy runs before workspace scoping.';



COMMENT ON COLUMN "ai"."key_usage_log"."token_input" IS 'Input tokens consumed by the provider for this run. Written by cloud worker.';



COMMENT ON COLUMN "ai"."key_usage_log"."token_output" IS 'Output tokens generated by the provider for this run. Written by cloud worker.';



COMMENT ON COLUMN "ai"."key_usage_log"."credit_cost" IS 'Platform credit cost computed from token usage. NULL for BYOK runs where the lenser supplies their own key and platform credits are not consumed.';



CREATE TABLE IF NOT EXISTS "ai"."keys" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "label" "text",
    "encrypted_key_id" "uuid" NOT NULL,
    "key_suffix" "text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "revoked_at" timestamp with time zone,
    "scope" "ai"."key_scope_enum" DEFAULT 'user'::"ai"."key_scope_enum",
    "scoped_entity_id" "uuid",
    "status" "ai"."key_status_enum" DEFAULT 'active'::"ai"."key_status_enum",
    "last_used_at" timestamp with time zone,
    "expires_at" timestamp with time zone,
    "key_prefix" "text",
    "provider_id" "uuid" NOT NULL,
    CONSTRAINT "keys_suffix_check" CHECK ((("char_length"("key_suffix") >= 1) AND ("char_length"("key_suffix") <= 4)))
);


ALTER TABLE "ai"."keys" OWNER TO "postgres";


COMMENT ON TABLE "ai"."keys" IS 'Encrypted BYOK API keys for AI providers. Raw keys are stored in vault.secrets via pgsodium; only the last 4 characters (key_suffix) are stored in plain text for masked display. The encrypted_key_id column references vault.secrets(id).';



COMMENT ON COLUMN "ai"."keys"."encrypted_key_id" IS 'Reference to vault.secrets(id). The raw API key is encrypted at rest via pgsodium. Decryption is restricted to service_role only.';



COMMENT ON COLUMN "ai"."keys"."key_suffix" IS 'Last 4 characters of the API key, stored in plain text for masked display (e.g., ••••abcd). Never store more than 4 characters.';



COMMENT ON COLUMN "ai"."keys"."provider_id" IS 'FK to ai.providers(id). NULL if provider row is later removed.';



CREATE TABLE IF NOT EXISTS "ai"."modality_pricing" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "model_id" "uuid" NOT NULL,
    "output_modality" "text" NOT NULL,
    "credit_rate" numeric NOT NULL,
    "rate_unit" "text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "modality_pricing_credit_rate_check" CHECK (("credit_rate" >= (0)::numeric)),
    CONSTRAINT "modality_pricing_output_modality_check" CHECK (("output_modality" = ANY (ARRAY['text'::"text", 'image'::"text", 'video'::"text", 'audio'::"text", 'music'::"text"]))),
    CONSTRAINT "modality_pricing_rate_unit_check" CHECK (("rate_unit" = ANY (ARRAY['per_image'::"text", 'per_second'::"text", 'per_1k_chars'::"text", 'per_request'::"text"])))
);


ALTER TABLE "ai"."modality_pricing" OWNER TO "postgres";


COMMENT ON TABLE "ai"."modality_pricing" IS 'Credit pricing per output unit for each model+modality combination. fn_complete_async_run() reads this to compute credit_cost at completion time.';



CREATE TABLE IF NOT EXISTS "ai"."model_pricing" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "model_id" "uuid" NOT NULL,
    "input_cost_per_1k_tokens" numeric(12,8) NOT NULL,
    "output_cost_per_1k_tokens" numeric(12,8) NOT NULL,
    "effective_from" timestamp with time zone DEFAULT "now"() NOT NULL,
    "effective_to" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "unit_type" "ai"."unit_type_enum" DEFAULT 'tokens'::"ai"."unit_type_enum" NOT NULL,
    "cost_per_unit" numeric(18,10),
    CONSTRAINT "model_pricing_costs_nonneg" CHECK ((("input_cost_per_1k_tokens" >= (0)::numeric) AND ("output_cost_per_1k_tokens" >= (0)::numeric))),
    CONSTRAINT "model_pricing_dates_valid" CHECK ((("effective_to" IS NULL) OR ("effective_to" > "effective_from")))
);


ALTER TABLE "ai"."model_pricing" OWNER TO "postgres";


COMMENT ON TABLE "ai"."model_pricing" IS 'Provider list prices per 1K tokens. Append-only time-series; close rows by setting effective_to.';



COMMENT ON COLUMN "ai"."model_pricing"."unit_type" IS 'Type of billing unit: tokens (text/chat), image (per image), video_second (per second), audio_second (per second). Typed as ai.unit_type_enum.';



COMMENT ON COLUMN "ai"."model_pricing"."cost_per_unit" IS 'Cost per unit (USD) for non-token models. NULL for token-based pricing. For image: cost_per_image; for video/audio: cost_per_second.';



CREATE TABLE IF NOT EXISTS "ai"."models" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "name" "text" NOT NULL,
    "version" "text",
    "provider_url" "text",
    "description" "text" DEFAULT ''::"text" NOT NULL,
    "temperature" numeric DEFAULT 0.7 NOT NULL,
    "max_tokens" integer DEFAULT 4096 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "key" "text",
    "provider_id" "uuid" NOT NULL,
    "context_window_tokens" integer,
    "supports_tools" boolean DEFAULT false NOT NULL,
    "supports_json_schema" boolean DEFAULT false NOT NULL,
    "supports_vision" boolean DEFAULT false NOT NULL,
    "is_active" boolean DEFAULT false NOT NULL,
    "capabilities" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "input_modalities" "text"[] DEFAULT ARRAY['text'::"text"] NOT NULL,
    "output_modalities" "text"[] DEFAULT ARRAY['text'::"text"] NOT NULL,
    "docs_url" "text",
    "support_level" "text" DEFAULT 'runnable'::"text" NOT NULL,
    "supports_streaming" boolean DEFAULT false NOT NULL,
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    "use_cases" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "developer_summary" "text" DEFAULT ''::"text" NOT NULL,
    "user_summary" "text" DEFAULT ''::"text" NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    CONSTRAINT "models_capabilities_check" CHECK (("capabilities" <@ ARRAY['chat'::"text", 'reasoning'::"text", 'tools'::"text", 'vision'::"text", 'json_schema'::"text", 'image_generation'::"text", 'video_generation'::"text", 'audio_generation'::"text", 'music_generation'::"text", 'code'::"text", 'text'::"text", 'image'::"text", 'music'::"text"])),
    CONSTRAINT "models_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'preview'::"text", 'deprecated'::"text", 'legacy'::"text"]))),
    CONSTRAINT "models_support_level_check" CHECK (("support_level" = ANY (ARRAY['runnable'::"text", 'byok_only'::"text", 'catalog_only'::"text", 'deprecated'::"text"])))
);


ALTER TABLE "ai"."models" OWNER TO "postgres";


COMMENT ON COLUMN "ai"."models"."key" IS 'Unique provider API identifier (e.g. ''gpt-4o'', ''claude-sonnet-4-6''). Renamed from model_key in migration 20260322000059.';



COMMENT ON COLUMN "ai"."models"."provider_id" IS 'FK to ai.providers when using the extended schema.';



COMMENT ON COLUMN "ai"."models"."context_window_tokens" IS 'Max context window in tokens.';



COMMENT ON COLUMN "ai"."models"."is_active" IS 'Whether this model is available for platform use.';



COMMENT ON COLUMN "ai"."models"."input_modalities" IS 'Extensible input modality list. Source of truth for CapabilityMapper validation. Replaces rigid supports_vision boolean for new code paths. Valid values: text, image, document, audio, video. Extend freely — no CHECK constraint.';



COMMENT ON COLUMN "ai"."models"."output_modalities" IS 'Extensible output modality list. Valid values: text, image, audio, video. Extend freely — no CHECK constraint.';



COMMENT ON COLUMN "ai"."models"."docs_url" IS 'Canonical upstream documentation URL for this model entry.';



COMMENT ON COLUMN "ai"."models"."support_level" IS 'Catalog support tier for the model: runnable, byok_only, catalog_only, deprecated.';



COMMENT ON COLUMN "ai"."models"."supports_streaming" IS 'Whether LenserFight can stream this model through a supported invocation path.';



COMMENT ON COLUMN "ai"."models"."status" IS 'Catalog lifecycle state: active, preview, deprecated, legacy.';



COMMENT ON COLUMN "ai"."models"."use_cases" IS 'Normalized recommended use-case tags for UI cards and CLI filtering.';



COMMENT ON COLUMN "ai"."models"."developer_summary" IS 'Developer-facing explanation for when and how to use the model.';



COMMENT ON COLUMN "ai"."models"."user_summary" IS 'User-facing explanation for what the model is best at.';



COMMENT ON COLUMN "ai"."models"."metadata" IS 'Flexible metadata for source provenance, auth mode, platform/gateway compatibility, pricing snapshots, and provider-specific notes.';



CREATE TABLE IF NOT EXISTS "ai"."providers" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "key" "text" NOT NULL,
    "display_name" "text" NOT NULL,
    "base_url" "text",
    "docs_url" "text",
    "is_active" boolean DEFAULT true NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "support_level" "text" DEFAULT 'runnable'::"text" NOT NULL,
    "logo_slug" "text",
    CONSTRAINT "providers_support_level_check" CHECK (("support_level" = ANY (ARRAY['runnable'::"text", 'byok_only'::"text", 'catalog_only'::"text", 'deprecated'::"text"])))
);


ALTER TABLE "ai"."providers" OWNER TO "postgres";


COMMENT ON TABLE "ai"."providers" IS 'AI provider registry. OSS-safe: no secrets, metadata only.';



COMMENT ON COLUMN "ai"."providers"."key" IS 'Short machine key, e.g. openai, anthropic, google.';



COMMENT ON COLUMN "ai"."providers"."base_url" IS 'Override for self-hosted or proxy endpoints.';



COMMENT ON COLUMN "ai"."providers"."support_level" IS 'Catalog support tier for the provider: runnable, byok_only, catalog_only, deprecated.';



COMMENT ON COLUMN "ai"."providers"."logo_slug" IS 'Stable UI asset slug for provider logos/cards.';



CREATE TABLE IF NOT EXISTS "analytics"."agent_cost_daily" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "model_key" "text" NOT NULL,
    "provider" "text" NOT NULL,
    "period_date" "date" NOT NULL,
    "total_tokens_in" bigint DEFAULT 0 NOT NULL,
    "total_tokens_out" bigint DEFAULT 0 NOT NULL,
    "total_credits" numeric DEFAULT 0 NOT NULL,
    "run_count" integer DEFAULT 0 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "acd_run_count_nonneg" CHECK (("run_count" >= 0)),
    CONSTRAINT "acd_total_credits_nonneg" CHECK (("total_credits" >= (0)::numeric)),
    CONSTRAINT "ck_analytics_agent_cost_daily_run_count_nonneg" CHECK ((("run_count" IS NULL) OR ("run_count" >= 0))),
    CONSTRAINT "ck_analytics_agent_cost_daily_total_credits_nonneg" CHECK ((("total_credits" IS NULL) OR ("total_credits" >= (0)::numeric))),
    CONSTRAINT "ck_analytics_agent_cost_daily_total_tokens_in_nonneg" CHECK ((("total_tokens_in" IS NULL) OR ("total_tokens_in" >= 0))),
    CONSTRAINT "ck_analytics_agent_cost_daily_total_tokens_out_nonneg" CHECK ((("total_tokens_out" IS NULL) OR ("total_tokens_out" >= 0)))
);


ALTER TABLE "analytics"."agent_cost_daily" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "analytics"."contact" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid",
    "name" "text" NOT NULL,
    "email" "text" NOT NULL,
    "subject" "text" NOT NULL,
    "message" "text" NOT NULL,
    "kvkk_approved" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "ip_address" "inet",
    "user_agent" "text",
    CONSTRAINT "contact_email_check" CHECK (("email" ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'::"text"))
);


ALTER TABLE "analytics"."contact" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "analytics"."eval_quality_daily" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "evaluation_id" "uuid" NOT NULL,
    "period_date" "date" NOT NULL,
    "run_count" integer DEFAULT 0 NOT NULL,
    "pass_count" integer DEFAULT 0 NOT NULL,
    "total_cases" integer DEFAULT 0 NOT NULL,
    "passed_cases" integer DEFAULT 0 NOT NULL,
    "mean_score" numeric,
    CONSTRAINT "ck_analytics_eval_quality_daily_mean_score_nonneg" CHECK ((("mean_score" IS NULL) OR ("mean_score" >= (0)::numeric))),
    CONSTRAINT "ck_analytics_eval_quality_daily_pass_count_nonneg" CHECK ((("pass_count" IS NULL) OR ("pass_count" >= 0))),
    CONSTRAINT "ck_analytics_eval_quality_daily_run_count_nonneg" CHECK ((("run_count" IS NULL) OR ("run_count" >= 0))),
    CONSTRAINT "ck_analytics_eval_quality_daily_total_cases_nonneg" CHECK ((("total_cases" IS NULL) OR ("total_cases" >= 0)))
);


ALTER TABLE "analytics"."eval_quality_daily" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "analytics"."lenser_activity" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "activity_type" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "context" "jsonb" DEFAULT '{}'::"jsonb",
    "entity_type" "text",
    "entity_id" "uuid",
    "ip_hash" "text",
    CONSTRAINT "lenser_activity_type_check" CHECK (("activity_type" = ANY (ARRAY['login'::"text", 'page_view'::"text", 'ray_run'::"text", 'battle_vote'::"text", 'thread_create'::"text", 'follow'::"text", 'agent_deploy'::"text", 'battle_create'::"text", 'submission'::"text", 'profile_update'::"text"])))
);


ALTER TABLE "analytics"."lenser_activity" OWNER TO "postgres";


COMMENT ON COLUMN "analytics"."lenser_activity"."context" IS 'Arbitrary metadata: page, referrer, device, user_agent_hash, etc.';



COMMENT ON COLUMN "analytics"."lenser_activity"."entity_type" IS 'Type of entity the activity relates to: prompt_template, thread, battle, etc.';



COMMENT ON COLUMN "analytics"."lenser_activity"."entity_id" IS 'ID of the entity the activity relates to.';



COMMENT ON COLUMN "analytics"."lenser_activity"."ip_hash" IS 'SHA-256 hash of IP address for GDPR-safe geo/fraud context.';



CREATE TABLE IF NOT EXISTS "analytics"."lenser_join_log" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid",
    "joined_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "join_order" bigint NOT NULL
);


ALTER TABLE "analytics"."lenser_join_log" OWNER TO "postgres";


CREATE SEQUENCE IF NOT EXISTS "analytics"."lenser_join_sequence"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "analytics"."lenser_join_sequence" OWNER TO "postgres";


ALTER SEQUENCE "analytics"."lenser_join_sequence" OWNED BY "analytics"."lenser_join_log"."join_order";



CREATE TABLE IF NOT EXISTS "analytics"."page_views" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid",
    "user_id" "uuid",
    "target_type" "public"."page_view_target_enum" NOT NULL,
    "target_id" "text",
    "path" "text" NOT NULL,
    "referrer" "text",
    "user_agent" "text",
    "client_ip" "inet",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "analytics"."page_views" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "analytics"."product_feedback" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "product_tag" "analytics"."product_tag_enum" DEFAULT 'general'::"analytics"."product_tag_enum" NOT NULL,
    "page" "text",
    "user_id" "uuid",
    "message" "text",
    "start_date" timestamp with time zone,
    "end_date" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "status" "analytics"."feedback_status_enum" DEFAULT 'pending'::"analytics"."feedback_status_enum" NOT NULL,
    CONSTRAINT "feedback_message_check" CHECK ((("message" IS NULL) OR (("btrim"("message") = "message") AND (("char_length"("message") >= 10) AND ("char_length"("message") <= 2000)))))
);


ALTER TABLE "analytics"."product_feedback" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "analytics"."share_events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "shared_link_id" "uuid" NOT NULL,
    "event_type" "text" NOT NULL,
    "viewer_lenser_id" "uuid",
    "viewer_session_id" "text",
    "ip_hash" "text",
    "country" "text",
    "city" "text",
    "user_agent" "text",
    "referer" "text",
    "ref_host" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "analytics"."share_events" OWNER TO "postgres";


COMMENT ON TABLE "analytics"."share_events" IS 'Telemetry for shared link usage (generated/opened/etc.), with privacy-aware IP and geo, RLS via link owner.';



CREATE TABLE IF NOT EXISTS "analytics"."tag_activity_events" (
    "id" bigint NOT NULL,
    "tag_id" "uuid" NOT NULL,
    "entity_type" "content"."entity_type_enum" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "actor_id" "uuid",
    "activity_type" "text" NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tag_activity_events_activity_type_check" CHECK (("activity_type" = ANY (ARRAY['created'::"text", 'viewed'::"text", 'reacted'::"text", 'replied'::"text", 'shared'::"text", 'bookmarked'::"text", 'commented'::"text"])))
);


ALTER TABLE "analytics"."tag_activity_events" OWNER TO "postgres";


CREATE SEQUENCE IF NOT EXISTS "analytics"."tag_activity_events_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "analytics"."tag_activity_events_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "analytics"."tag_activity_events_id_seq" OWNED BY "analytics"."tag_activity_events"."id";



CREATE TABLE IF NOT EXISTS "analytics"."workflow_perf_daily" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "period_date" "date" NOT NULL,
    "run_count" integer DEFAULT 0 NOT NULL,
    "failed_count" integer DEFAULT 0 NOT NULL,
    "p50_duration_ms" integer,
    "p95_duration_ms" integer,
    CONSTRAINT "ck_analytics_workflow_perf_daily_failed_count_nonneg" CHECK ((("failed_count" IS NULL) OR ("failed_count" >= 0))),
    CONSTRAINT "ck_analytics_workflow_perf_daily_run_count_nonneg" CHECK ((("run_count" IS NULL) OR ("run_count" >= 0)))
);


ALTER TABLE "analytics"."workflow_perf_daily" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "audit"."account_lifecycle" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "profile_id" "uuid" NOT NULL,
    "user_id" "uuid",
    "event_type" "text" NOT NULL,
    "from_status" "lensers"."lenser_status",
    "to_status" "lensers"."lenser_status",
    "actor_source" "text" NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "account_lifecycle_audit_actor_source_check" CHECK (("actor_source" = ANY (ARRAY['user_action'::"text", 'signin_restore'::"text", 'system_job'::"text"]))),
    CONSTRAINT "account_lifecycle_audit_event_type_check" CHECK (("event_type" = ANY (ARRAY['deletion_scheduled'::"text", 'account_deactivated'::"text", 'restored_from_pending_deletion'::"text", 'restored_from_deactivated'::"text", 'deletion_completed'::"text"])))
);


ALTER TABLE "audit"."account_lifecycle" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "audit"."admin_actions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "admin_lenser_id" "uuid" NOT NULL,
    "action_type" "text" NOT NULL,
    "target_entity_id" "uuid",
    "target_entity_type" "text",
    "reason" "text",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
);


ALTER TABLE "audit"."admin_actions" OWNER TO "postgres";


COMMENT ON TABLE "audit"."admin_actions" IS 'Admin operations audit log. Append-only. Every admin action must be logged here. KEPT SEPARATE from audit.events because: (1) admin_lenser_id is NOT NULL — all admin actions require a named human actor; (2) reason column is required for all admin actions (compliance requirement); (3) distinct RLS: only service_role reads admin_actions (audit.events has broader read policies); (4) potential for distinct retention rules (admin logs may need longer retention than general events). If all four distinctions are resolved, migrate rows into audit.events and drop this table.';



CREATE TABLE IF NOT EXISTS "audit"."attestations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "entity_type" "text" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "attested_by" "uuid" NOT NULL,
    "attestation_type" "text" NOT NULL,
    "proof_payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "attestations_type_check" CHECK (("attestation_type" = ANY (ARRAY['battle_published'::"text", 'battle_finalized'::"text", 'agent_created'::"text", 'agent_suspended'::"text", 'result_invalidated'::"text"])))
);


ALTER TABLE "audit"."attestations" OWNER TO "postgres";


COMMENT ON TABLE "audit"."attestations" IS 'Append-only attestation records for key platform lifecycle events. Created exclusively via SECURITY DEFINER RPCs. proof_payload captures the relevant context (snapshot_hash, handle, etc.) for future audit replay.';



CREATE TABLE IF NOT EXISTS "audit"."byok_key_usage" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "key_id" "uuid" NOT NULL,
    "battle_id" "uuid",
    "model_id" "text" NOT NULL,
    "called_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "token_count" integer DEFAULT 0 NOT NULL,
    "caller_role" "text" DEFAULT 'service_role'::"text" NOT NULL,
    CONSTRAINT "byok_key_usage_token_count_check" CHECK (("token_count" >= 0))
);


ALTER TABLE "audit"."byok_key_usage" OWNER TO "postgres";


COMMENT ON TABLE "audit"."byok_key_usage" IS 'BZ: Per-call audit trail for BYOK key usage. Written by fn_byok_log_usage (service_role only). Owners can SELECT their own key usage rows; no direct INSERT/UPDATE/DELETE from authenticated.';



CREATE TABLE IF NOT EXISTS "audit"."dedupe_log" (
    "id" bigint NOT NULL,
    "table_name" "text" NOT NULL,
    "removed_pk" "text" NOT NULL,
    "kept_pk" "text" NOT NULL,
    "dedupe_key" "jsonb" NOT NULL,
    "removed_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "audit"."dedupe_log" OWNER TO "postgres";


CREATE SEQUENCE IF NOT EXISTS "audit"."dedupe_log_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "audit"."dedupe_log_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "audit"."dedupe_log_id_seq" OWNED BY "audit"."dedupe_log"."id";



CREATE TABLE IF NOT EXISTS "audit"."disputes" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "target_entity_type" "text" NOT NULL,
    "target_entity_id" "uuid" NOT NULL,
    "disputer_lenser_id" "uuid" NOT NULL,
    "reason" "text" NOT NULL,
    "evidence_urls" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "status" "text" DEFAULT 'open'::"text" NOT NULL,
    "resolved_by" "uuid",
    "resolved_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "disputes_resolve_consistency" CHECK ((("status" = ANY (ARRAY['resolved'::"text", 'dismissed'::"text"])) = (("resolved_by" IS NOT NULL) AND ("resolved_at" IS NOT NULL)))),
    CONSTRAINT "disputes_status_check" CHECK (("status" = ANY (ARRAY['open'::"text", 'under_review'::"text", 'resolved'::"text", 'dismissed'::"text"])))
);


ALTER TABLE "audit"."disputes" OWNER TO "postgres";


COMMENT ON TABLE "audit"."disputes" IS 'Appeal trail for moderation decisions. Mutable: status transitions from open → under_review → resolved/dismissed. A dispute does not modify the original target record — it creates a parallel review record alongside the immutable audit trail.';



CREATE TABLE IF NOT EXISTS "audit"."events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "event_type" "text" NOT NULL,
    "entity_schema" "text",
    "entity_table" "text",
    "entity_id" "uuid",
    "actor_lenser_id" "uuid",
    "actor_actor_id" "uuid",
    "ip_hash" "text",
    "user_agent_hash" "text",
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "severity" "text" DEFAULT 'info'::"text" NOT NULL,
    "actor_type" "text",
    "actor_id" "uuid",
    CONSTRAINT "events_actor_type_check" CHECK ((("actor_type" IS NULL) OR ("actor_type" = ANY (ARRAY['lenser'::"text", 'ai_actor'::"text", 'system'::"text"])))),
    CONSTRAINT "events_no_dual_actor" CHECK ((NOT (("actor_lenser_id" IS NOT NULL) AND ("actor_actor_id" IS NOT NULL)))),
    CONSTRAINT "events_severity_check" CHECK (("severity" = ANY (ARRAY['info'::"text", 'warn'::"text", 'error'::"text", 'critical'::"text"])))
);


ALTER TABLE "audit"."events" OWNER TO "postgres";


COMMENT ON TABLE "audit"."events" IS 'Core immutable event log. Append-only. Partition by occurred_at when row count exceeds ~10M rows.';



COMMENT ON COLUMN "audit"."events"."actor_lenser_id" IS 'DEPRECATED: use actor_type=''lenser'' + actor_id instead. Will be removed after all callers migrate to log_event_v2.';



COMMENT ON COLUMN "audit"."events"."actor_actor_id" IS 'DEPRECATED: use actor_type=''ai_actor'' + actor_id instead. Will be removed after all callers migrate to log_event_v2.';



COMMENT ON COLUMN "audit"."events"."ip_hash" IS 'SHA-256 of client IP. GDPR-safe: not reversible without rainbow table. Never store raw IP.';



COMMENT ON COLUMN "audit"."events"."payload" IS 'Non-sensitive event details. MUST NOT contain API keys, tokens, or PII.';



COMMENT ON COLUMN "audit"."events"."actor_type" IS 'Unified actor type: ''lenser'', ''ai_actor'', or ''system''. Supersedes actor_lenser_id + actor_actor_id split columns.';



COMMENT ON COLUMN "audit"."events"."actor_id" IS 'UUID of the unified actor. Interpret per actor_type.';



CREATE TABLE IF NOT EXISTS "audit"."hash_chains" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "event_id" "uuid" NOT NULL,
    "prev_hash" "text",
    "payload_hash" "text" NOT NULL,
    "sequence_no" bigint NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "chain_kind" "text" DEFAULT 'event'::"text" NOT NULL,
    "subject_lenser_id" "uuid",
    "subject_device_id" "uuid",
    "sub_sequence_no" bigint,
    CONSTRAINT "hash_chains_chain_kind_check" CHECK (("chain_kind" = ANY (ARRAY['event'::"text", 'gateway'::"text"])))
);


ALTER TABLE "audit"."hash_chains" OWNER TO "postgres";


COMMENT ON TABLE "audit"."hash_chains" IS 'Append-only hash chain over audit.events. Provides tamper-evident ordering. sequence_no is monotonically increasing across the entire chain. Partition by sequence_no range when it exceeds ~5M rows.';



COMMENT ON COLUMN "audit"."hash_chains"."chain_kind" IS 'Logical chain identifier. ''event'' is the original global chain. ''gateway'' scopes per-Lenser sub-chains for Trust-Gateway events (device registration, trust elevation, attestation verification, daemon lifecycle).';



COMMENT ON COLUMN "audit"."hash_chains"."sub_sequence_no" IS 'Per-(chain_kind, subject_lenser_id) monotonic sequence. NULL for legacy ''event'' chain where global sequence_no is the source of truth.';



CREATE TABLE IF NOT EXISTS "audit"."idempotency_keys" (
    "scope" "text" NOT NULL,
    "idempotency_key" "text" NOT NULL,
    "reference_type" "text",
    "reference_id" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "audit"."idempotency_keys" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "audit"."idempotency_requests" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "idempotency_key" "text" NOT NULL,
    "idempotency_key_hash" "text" NOT NULL,
    "account_id" "uuid" NOT NULL,
    "workspace_id" "uuid",
    "actor_chainer_id" "uuid" NOT NULL,
    "route_signature" "text" NOT NULL,
    "method" "text" NOT NULL,
    "request_fingerprint" "text" NOT NULL,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "http_status" integer,
    "response_headers" "jsonb",
    "response_body" "jsonb",
    "error_body" "jsonb",
    "provider_name" "text",
    "provider_operation_key" "text",
    "provider_request_id" "text",
    "locked_until" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "expires_at" timestamp with time zone DEFAULT ("now"() + '24:00:00'::interval) NOT NULL,
    CONSTRAINT "idempotency_requests_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'processing'::"text", 'completed'::"text", 'failed'::"text"])))
);


ALTER TABLE "audit"."idempotency_requests" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "audit"."moderation_decisions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "target_entity_schema" "text" NOT NULL,
    "target_entity_table" "text" NOT NULL,
    "target_entity_id" "uuid" NOT NULL,
    "decision_type" "text" NOT NULL,
    "reason" "text",
    "evidence_urls" "text"[],
    "moderator_lenser_id" "uuid",
    "is_ai_moderated" boolean DEFAULT false NOT NULL,
    "ai_confidence" numeric(5,4),
    "appeal_deadline_at" timestamp with time zone,
    "is_appealed" boolean DEFAULT false NOT NULL,
    "appealed_at" timestamp with time zone,
    "appeal_outcome" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "moderation_accountability_check" CHECK ((("is_ai_moderated" = true) OR ("moderator_lenser_id" IS NOT NULL))),
    CONSTRAINT "moderation_decisions_ai_confidence_range" CHECK ((("ai_confidence" IS NULL) OR (("ai_confidence" >= (0)::numeric) AND ("ai_confidence" <= (1)::numeric)))),
    CONSTRAINT "moderation_decisions_appeal_outcome_check" CHECK ((("appeal_outcome" IS NULL) OR ("appeal_outcome" = ANY (ARRAY['upheld'::"text", 'overturned'::"text", 'pending'::"text"])))),
    CONSTRAINT "moderation_decisions_type_check" CHECK (("decision_type" = ANY (ARRAY['approved'::"text", 'rejected'::"text", 'flagged'::"text", 'removed'::"text", 'restored'::"text", 'warned'::"text"]))),
    CONSTRAINT "moderation_no_dual_actor" CHECK ((NOT (("is_ai_moderated" = true) AND ("moderator_lenser_id" IS NOT NULL))))
);


ALTER TABLE "audit"."moderation_decisions" OWNER TO "postgres";


COMMENT ON TABLE "audit"."moderation_decisions" IS 'Moderation log for content, battles, and actors. Includes appeal tracking.';



COMMENT ON CONSTRAINT "moderation_accountability_check" ON "audit"."moderation_decisions" IS 'Every human-moderated decision (is_ai_moderated=false) must name a moderator. Prevents unattributed decisions that create legal/compliance risk.';



COMMENT ON CONSTRAINT "moderation_no_dual_actor" ON "audit"."moderation_decisions" IS 'AI-moderated decisions must not also carry a human moderator_lenser_id. Prevents semantically contradictory attribution.';



CREATE TABLE IF NOT EXISTS "audit"."orphaned_reactions" (
    "id" "uuid",
    "entity_type" "text",
    "entity_id" "uuid",
    "lenser_id" "uuid",
    "reaction" "text",
    "archived_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "audit"."orphaned_reactions" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "audit"."security_events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "event_type" "audit"."security_event_type_enum" NOT NULL,
    "lenser_id" "uuid",
    "key_ref_id" "uuid",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "ip_hash" "text",
    "severity" "text" DEFAULT 'info'::"text" NOT NULL,
    CONSTRAINT "security_events_severity_check" CHECK (("severity" = ANY (ARRAY['info'::"text", 'warn'::"text", 'error'::"text", 'critical'::"text"])))
);


ALTER TABLE "audit"."security_events" OWNER TO "postgres";


COMMENT ON TABLE "audit"."security_events" IS 'BYOK lifecycle and security event log. Append-only. key_ref_id references agents.byok_key_refs; vault_secret_name is never stored here.';



COMMENT ON COLUMN "audit"."security_events"."event_type" IS 'Security event category. Typed as audit.security_event_type_enum. Extend by: ALTER TYPE audit.security_event_type_enum ADD VALUE ''new_value''.';



COMMENT ON COLUMN "audit"."security_events"."metadata" IS 'Non-sensitive metadata only. No vault names, no raw keys, no tokens.';



CREATE TABLE IF NOT EXISTS "audit"."webhook_outbox" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "event_type" "text" NOT NULL,
    "payload" "jsonb" NOT NULL,
    "target_url" "text" NOT NULL,
    "attempts" integer DEFAULT 0 NOT NULL,
    "max_attempts" integer DEFAULT 5 NOT NULL,
    "next_attempt_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "delivered_at" timestamp with time zone,
    "last_error" "text",
    "dead_lettered_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "audit"."webhook_outbox" OWNER TO "postgres";


COMMENT ON TABLE "audit"."webhook_outbox" IS 'CB: Pending webhook deliveries. Drained by webhook-drain-worker.';



CREATE TABLE IF NOT EXISTS "authz"."developer_tokens" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "label" "text",
    "token_hash" "text" NOT NULL,
    "token_prefix" "text" NOT NULL,
    "status" "authz"."developer_token_status_enum" DEFAULT 'active'::"authz"."developer_token_status_enum" NOT NULL,
    "issued_from_request_id" "uuid",
    "expires_at" timestamp with time zone NOT NULL,
    "revoked_at" timestamp with time zone,
    "last_used_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "authz"."developer_tokens" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "authz"."device_approval_requests" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "user_code" "text" NOT NULL,
    "request_secret_hash" "text" NOT NULL,
    "label" "text",
    "requested_by_user_id" "uuid",
    "requested_by_lenser_id" "uuid",
    "requested_token_ttl_hours" integer DEFAULT 24 NOT NULL,
    "status" "authz"."device_approval_request_status_enum" DEFAULT 'pending'::"authz"."device_approval_request_status_enum" NOT NULL,
    "approved_at" timestamp with time zone,
    "approved_by_user_id" "uuid",
    "approved_by_lenser_id" "uuid",
    "developer_token_id" "uuid",
    "expires_at" timestamp with time zone NOT NULL,
    "exchanged_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "login_access_token" "text",
    "login_refresh_token" "text"
);


ALTER TABLE "authz"."device_approval_requests" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "automation"."cron_runs" (
    "id" bigint NOT NULL,
    "job_name" "text" NOT NULL,
    "started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "finished_at" timestamp with time zone,
    "status" "text" DEFAULT 'running'::"text" NOT NULL,
    "error_msg" "text",
    CONSTRAINT "cron_runs_status_check" CHECK (("status" = ANY (ARRAY['running'::"text", 'ok'::"text", 'skipped_locked'::"text", 'error'::"text"])))
);


ALTER TABLE "automation"."cron_runs" OWNER TO "postgres";


COMMENT ON TABLE "automation"."cron_runs" IS 'D1: cron job execution audit. RLS-enabled; service_role only. Read-only from any other role.';



CREATE SEQUENCE IF NOT EXISTS "automation"."cron_runs_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "automation"."cron_runs_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "automation"."cron_runs_id_seq" OWNED BY "automation"."cron_runs"."id";



CREATE TABLE IF NOT EXISTS "automation"."event_dispatches" (
    "event_id" "uuid" NOT NULL,
    "rule_id" "uuid" NOT NULL,
    "status" "text" NOT NULL,
    "attempted_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "error" "text",
    "outbox_id" "uuid",
    CONSTRAINT "event_dispatches_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'dispatched'::"text", 'failed'::"text", 'skipped'::"text"])))
);


ALTER TABLE "automation"."event_dispatches" OWNER TO "postgres";


COMMENT ON TABLE "automation"."event_dispatches" IS 'Phase U2: idempotency ledger. PK (event_id, rule_id) prevents an event firing the same rule twice. outbox_id is a soft pointer to audit.webhook_outbox.id when action_kind=webhook; not a real FK so the audit schema stays decoupled.';



CREATE TABLE IF NOT EXISTS "automation"."events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "event_type" "text" NOT NULL,
    "source_schema" "text",
    "source_table" "text",
    "source_id" "uuid",
    "source_lenser_id" "uuid",
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "recorded_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "automation"."events" OWNER TO "postgres";


COMMENT ON TABLE "automation"."events" IS 'Phase U1: append-only event log. Producer triggers call automation.fn_emit_event. source_lenser_id is denormalized at emit time so owner-only RLS does not need cross-schema joins. Payloads carry IDs and timestamps only; no PII or user-supplied content.';



COMMENT ON COLUMN "automation"."events"."event_type" IS 'Dot-namespaced event type, e.g. battle.finalized, workflow_run.completed, workflow_run.failed, approval.timed_out, approval.granted, battle.flagged.';



COMMENT ON COLUMN "automation"."events"."source_lenser_id" IS 'Denormalized owner of the source row at emit time. NULL means system-level / no obvious owner. Trigger rules with NULL filter ignore ownership.';



CREATE TABLE IF NOT EXISTS "automation"."trigger_rules" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "match_event_type" "text" NOT NULL,
    "match_filter" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "action_kind" "text" NOT NULL,
    "action_config" "jsonb" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "trigger_rules_action_kind_check" CHECK (("action_kind" = ANY (ARRAY['dispatch_workflow'::"text", 'webhook'::"text", 'notify'::"text"]))),
    CONSTRAINT "trigger_rules_match_filter_keys_valid" CHECK ("automation"."fn_check_filter_keys"("match_filter"))
);


ALTER TABLE "automation"."trigger_rules" OWNER TO "postgres";


COMMENT ON TABLE "automation"."trigger_rules" IS 'Phase U2: lenser-owned trigger rules. Match an automation.events row by event_type + filter DSL, then dispatch action_kind in (dispatch_workflow, webhook, notify). Action shape is in action_config; see fn_dispatch_action.';



COMMENT ON COLUMN "automation"."trigger_rules"."match_filter" IS 'Frozen v1 DSL: { "<json-pointer>": { "<op>": <value> } } AND-ed. Ops: eq, neq, gt, lt, contains. Empty object or NULL matches all events of the given type.';



COMMENT ON COLUMN "automation"."trigger_rules"."action_config" IS 'Per-kind shape. dispatch_workflow: { workflow_id, context_inputs?, global_model_id? }. webhook: { url, extra_headers? }. notify: { type?, title, body?, action_url?, metadata? }.';



CREATE TABLE IF NOT EXISTS "battles"."ai_handicap_policies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "max_tokens_per_second" integer,
    "injected_delay_ms" integer DEFAULT 0 NOT NULL,
    "max_context_tokens" integer,
    "allowed_model_tier" "text",
    "time_budget_ms" integer,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ai_handicap_policies_allowed_model_tier_check" CHECK ((("allowed_model_tier" IS NULL) OR ("allowed_model_tier" = ANY (ARRAY['free'::"text", 'paid'::"text", 'enterprise'::"text"])))),
    CONSTRAINT "ai_handicap_policies_injected_delay_ms_check" CHECK (("injected_delay_ms" >= 0)),
    CONSTRAINT "ai_handicap_policies_max_context_tokens_check" CHECK ((("max_context_tokens" IS NULL) OR ("max_context_tokens" > 0))),
    CONSTRAINT "ai_handicap_policies_max_tokens_per_second_check" CHECK ((("max_tokens_per_second" IS NULL) OR ("max_tokens_per_second" > 0))),
    CONSTRAINT "ai_handicap_policies_time_budget_ms_check" CHECK ((("time_budget_ms" IS NULL) OR ("time_budget_ms" > 0)))
);


ALTER TABLE "battles"."ai_handicap_policies" OWNER TO "postgres";


COMMENT ON TABLE "battles"."ai_handicap_policies" IS 'Per-battle AI handicap configuration. Consumed by the CF Worker execution proxy. NULL fields on each axis = no restriction applied. Standard fair defaults for human_vs_ai: injected_delay_ms=2000, time_budget_ms=300000.';



CREATE TABLE IF NOT EXISTS "battles"."ai_judge_verdicts" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "criterion_id" "uuid",
    "score" numeric(4,2) NOT NULL,
    "rationale" "text",
    "model_key" "text" NOT NULL,
    "run_id" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ai_judge_verdicts_score_check" CHECK ((("score" >= (0)::numeric) AND ("score" <= (10)::numeric)))
);


ALTER TABLE "battles"."ai_judge_verdicts" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."battle_event_subscriptions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "owner_id" "uuid" NOT NULL,
    "webhook_url" "text" NOT NULL,
    "event_types" "text"[] NOT NULL,
    "secret_hmac" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "revoked_at" timestamp with time zone
);


ALTER TABLE "battles"."battle_event_subscriptions" OWNER TO "postgres";


COMMENT ON TABLE "battles"."battle_event_subscriptions" IS 'CB: Webhook subscriptions for battle events. Owner manages own; service_role processes.';



CREATE TABLE IF NOT EXISTS "battles"."battle_execution_dead_letters" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "job_id" "uuid" NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "slot" "text",
    "error_code" "text",
    "error_message" "text",
    "attempt_count" integer DEFAULT 1 NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "resolved_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_battles_battle_execution_dead_letters_attempt_count_nonneg" CHECK ((("attempt_count" IS NULL) OR ("attempt_count" >= 0)))
);


ALTER TABLE "battles"."battle_execution_dead_letters" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."battle_execution_jobs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "slot" "text" NOT NULL,
    "status" "text" DEFAULT 'queued'::"text" NOT NULL,
    "worker_id" "text",
    "claimed_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "retry_count" integer DEFAULT 0 NOT NULL,
    "max_retries" integer DEFAULT 3 NOT NULL,
    "error_message" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "redacted_agent_snapshot" "jsonb",
    "redacted_agent_snapshot_hash" "text",
    CONSTRAINT "battle_execution_jobs_slot_check" CHECK (("slot" = ANY (ARRAY['A'::"text", 'B'::"text"]))),
    CONSTRAINT "battle_execution_jobs_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'claimed'::"text", 'running'::"text", 'completed'::"text", 'failed'::"text"]))),
    CONSTRAINT "ck_battles_battle_execution_jobs_retry_count_nonneg" CHECK ((("retry_count" IS NULL) OR ("retry_count" >= 0)))
);


ALTER TABLE "battles"."battle_execution_jobs" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."comments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "body" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "comments_body_check" CHECK ((("char_length"("body") >= 1) AND ("char_length"("body") <= 1000)))
);


ALTER TABLE "battles"."comments" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."contender_lens_assignments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "lens_id" "uuid" NOT NULL,
    "version_id" "uuid",
    "assignment_mode" "text" DEFAULT 'manual'::"text" NOT NULL,
    "assigned_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "input_snapshot" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    CONSTRAINT "cla_mode_check" CHECK (("assignment_mode" = ANY (ARRAY['manual'::"text", 'auto_agent'::"text", 'battle_required'::"text"])))
);


ALTER TABLE "battles"."contender_lens_assignments" OWNER TO "postgres";


COMMENT ON TABLE "battles"."contender_lens_assignments" IS 'Explicit lens assignment per contender. UNIQUE(contender_id) ensures one lens per contender since contender_id is already battle-scoped. assignment_mode=auto_agent means an AI Lenser selected the lens via policy. assignment_mode=battle_required means the battle creator mandated a lens.';



CREATE TABLE IF NOT EXISTS "battles"."contender_runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "run_id" "uuid" NOT NULL,
    "ordinal" integer DEFAULT 1 NOT NULL,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "credit_cost" bigint,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "contender_runs_ordinal_positive" CHECK (("ordinal" >= 1)),
    CONSTRAINT "contender_runs_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'running'::"text", 'succeeded'::"text", 'failed'::"text", 'cancelled'::"text"])))
);


ALTER TABLE "battles"."contender_runs" OWNER TO "postgres";


COMMENT ON TABLE "battles"."contender_runs" IS 'Links a contender to its execution run(s). Provides provenance: which runs produced the contender''s submission. ordinal tracks multi-round executions.';



COMMENT ON COLUMN "battles"."contender_runs"."credit_cost" IS 'Platform credits consumed by this run. Copied from execution.runs.credit_cost at completion for fast aggregation without joining execution schema.';



CREATE TABLE IF NOT EXISTS "battles"."cost_ledgers" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid",
    "run_id" "uuid",
    "event_type" "text" NOT NULL,
    "credit_amount" bigint NOT NULL,
    "running_total" bigint NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "cost_ledgers_amount_nonzero" CHECK (("credit_amount" <> 0)),
    CONSTRAINT "cost_ledgers_event_type_check" CHECK (("event_type" = ANY (ARRAY['reserved'::"text", 'charged'::"text", 'released'::"text", 'refunded'::"text", 'adjusted'::"text"])))
);


ALTER TABLE "battles"."cost_ledgers" OWNER TO "postgres";


COMMENT ON TABLE "battles"."cost_ledgers" IS 'Append-only cost ledger per battle. Each row is an immutable cost event. running_total tracks cumulative spend for budget enforcement.';



COMMENT ON COLUMN "battles"."cost_ledgers"."running_total" IS 'Cumulative credit spend for this battle after this event. Enforced by application logic; verified by audit queries.';



CREATE TABLE IF NOT EXISTS "battles"."events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "event_type" "text" NOT NULL,
    "actor_id" "uuid",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "events_type_check" CHECK (("event_type" = ANY (ARRAY['status_change'::"text", 'contender_joined'::"text", 'submission_received'::"text", 'vote_cast'::"text", 'finalized'::"text", 'published'::"text", 'archived'::"text", 'invitation_sent'::"text", 'invitation_accepted'::"text", 'adapter_connected'::"text", 'contender_withdrawn'::"text", 'voting_opened'::"text", 'voting_closed'::"text", 'scoring_started'::"text", 'battle_cancelled'::"text", 'funding_allocated'::"text", 'cost_recorded'::"text", 'run_started'::"text", 'run_completed'::"text", 'run_failed'::"text", 'challenge_generated'::"text", 'challenge_locked'::"text", 'challenge_generation_failed'::"text"])))
);


ALTER TABLE "battles"."events" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."execution_configs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid",
    "provider_key" "text" NOT NULL,
    "model_key" "text" NOT NULL,
    "model_id" "uuid",
    "funding_source" "text" DEFAULT 'platform_credit'::"text" NOT NULL,
    "byok_key_ref_id" "uuid",
    "max_tokens" integer DEFAULT 4096,
    "temperature" numeric(3,2) DEFAULT 0.70,
    "created_at" timestamp with time zone DEFAULT "now"(),
    "updated_at" timestamp with time zone DEFAULT "now"(),
    CONSTRAINT "execution_configs_funding_source_check" CHECK (("funding_source" = ANY (ARRAY['user_byok_cloud'::"text", 'user_byok_local'::"text", 'platform_credit'::"text", 'sponsored'::"text"])))
);


ALTER TABLE "battles"."execution_configs" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."funding_policies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "funding_mode" "text" DEFAULT 'platform_credit'::"text" NOT NULL,
    "sponsor_lenser_id" "uuid",
    "max_budget_credits" bigint,
    "per_contender_limit" bigint,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "funding_policies_budget_positive" CHECK ((("max_budget_credits" IS NULL) OR ("max_budget_credits" > 0))),
    CONSTRAINT "funding_policies_mode_check" CHECK (("funding_mode" = ANY (ARRAY['byok'::"text", 'wallet_funded'::"text", 'sponsored'::"text", 'platform_subsidized'::"text"]))),
    CONSTRAINT "funding_policies_per_contender_positive" CHECK ((("per_contender_limit" IS NULL) OR ("per_contender_limit" > 0))),
    CONSTRAINT "funding_policies_sponsor_required" CHECK ((("funding_mode" <> 'sponsored'::"text") OR ("sponsor_lenser_id" IS NOT NULL)))
);


ALTER TABLE "battles"."funding_policies" OWNER TO "postgres";


COMMENT ON TABLE "battles"."funding_policies" IS 'Per-battle funding configuration. Determines who pays for AI execution costs. One policy per battle (UNIQUE on battle_id). funding_mode drives the wallet flow.';



COMMENT ON COLUMN "battles"."funding_policies"."max_budget_credits" IS 'Total credit budget for the battle. NULL = unlimited (platform_subsidized or byok).';



COMMENT ON COLUMN "battles"."funding_policies"."per_contender_limit" IS 'Max credits per contender. NULL = no per-contender limit (uses max_budget_credits only).';



CREATE TABLE IF NOT EXISTS "battles"."generated_challenges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "challenge_type" "text" NOT NULL,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "generator_lens_id" "uuid" NOT NULL,
    "generator_version_id" "uuid",
    "generator_model_id" "uuid" NOT NULL,
    "question_text" "text",
    "question_payload" "jsonb" DEFAULT '{}'::"jsonb",
    "answer_key_encrypted" "bytea",
    "answer_key_hash" "text",
    "difficulty" "text" DEFAULT 'medium'::"text",
    "language" "text" DEFAULT 'en'::"text",
    "time_limit_seconds" integer,
    "scoring_mode" "text",
    "content_hash" "text",
    "execution_run_id" "uuid",
    "input_snapshot" "jsonb" DEFAULT '{}'::"jsonb",
    "locked_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "created_by" "uuid",
    CONSTRAINT "chk_generated_challenge_difficulty" CHECK ((("difficulty" IS NULL) OR ("difficulty" = ANY (ARRAY['easy'::"text", 'medium'::"text", 'hard'::"text", 'expert'::"text"])))),
    CONSTRAINT "chk_generated_challenge_scoring_mode" CHECK ((("scoring_mode" IS NULL) OR ("scoring_mode" = ANY (ARRAY['auto_score'::"text", 'rubric_score'::"text", 'community_vote'::"text", 'ai_judge'::"text"])))),
    CONSTRAINT "chk_generated_challenge_status" CHECK (("status" = ANY (ARRAY['pending'::"text", 'generating'::"text", 'ready'::"text", 'locked'::"text", 'failed'::"text"]))),
    CONSTRAINT "chk_generated_challenge_type" CHECK (("challenge_type" = ANY (ARRAY['writing_contest'::"text", 'math_calculation'::"text", 'grammar_quiz'::"text", 'hand_drawing'::"text", 'fill_in_blanks'::"text", 'first_code_error'::"text", 'logic_puzzle'::"text", 'prompt_duel'::"text", 'debate'::"text", 'code_completion_benchmark'::"text", 'instruction_following_benchmark'::"text", 'reasoning_benchmark'::"text"])))
);


ALTER TABLE "battles"."generated_challenges" OWNER TO "postgres";


COMMENT ON TABLE "battles"."generated_challenges" IS 'AI-generated challenge questions for human-friendly battle types. Immutable once locked.';



CREATE TABLE IF NOT EXISTS "battles"."global_messages" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "sender_id" "uuid" NOT NULL,
    "sender_handle" "text" NOT NULL,
    "sender_role" "text" DEFAULT 'lenser'::"text" NOT NULL,
    "body" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "global_messages_body_check" CHECK ((("char_length"("body") >= 1) AND ("char_length"("body") <= 2000)))
);


ALTER TABLE "battles"."global_messages" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."invitations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "invited_by" "uuid" NOT NULL,
    "invited_email" "text",
    "invited_lenser_id" "uuid",
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "responded_at" timestamp with time zone,
    CONSTRAINT "invitations_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'accepted'::"text", 'declined'::"text", 'expired'::"text"])))
);


ALTER TABLE "battles"."invitations" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."invite_links" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "created_by" "uuid" NOT NULL,
    "type" "battles"."invite_link_type" DEFAULT 'public'::"battles"."invite_link_type" NOT NULL,
    "token" "text" DEFAULT "encode"("extensions"."gen_random_bytes"(10), 'hex'::"text") NOT NULL,
    "invite_url" "text" NOT NULL,
    "target_handle" "text",
    "accepted_at" timestamp with time zone,
    "click_count" integer DEFAULT 0 NOT NULL,
    "qr_scan_count" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "expires_at" timestamp with time zone,
    CONSTRAINT "invite_links_click_count_check" CHECK (("click_count" >= 0)),
    CONSTRAINT "invite_links_private_requires_target" CHECK ((("type" <> 'private'::"battles"."invite_link_type") OR ("target_handle" IS NOT NULL))),
    CONSTRAINT "invite_links_qr_scan_count_check" CHECK (("qr_scan_count" >= 0))
);


ALTER TABLE "battles"."invite_links" OWNER TO "postgres";


COMMENT ON TABLE "battles"."invite_links" IS 'Shareable battle invite tokens (QR, public link, private). Distinct from battles.invitations (direct person-to-person requests).';



CREATE TABLE IF NOT EXISTS "battles"."media_quality_rules" (
    "template_id" "uuid" NOT NULL,
    "modality" "text" NOT NULL,
    "min_width" integer,
    "min_height" integer,
    "max_duration_seconds" integer,
    "required_aspect_ratio" "text",
    "max_file_size_mb" integer,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "media_quality_rules_max_duration_seconds_check" CHECK ((("max_duration_seconds" IS NULL) OR ("max_duration_seconds" > 0))),
    CONSTRAINT "media_quality_rules_max_file_size_mb_check" CHECK ((("max_file_size_mb" IS NULL) OR ("max_file_size_mb" > 0))),
    CONSTRAINT "media_quality_rules_min_height_check" CHECK ((("min_height" IS NULL) OR ("min_height" > 0))),
    CONSTRAINT "media_quality_rules_min_width_check" CHECK ((("min_width" IS NULL) OR ("min_width" > 0))),
    CONSTRAINT "media_quality_rules_modality_check" CHECK (("modality" = ANY (ARRAY['image'::"text", 'video'::"text", 'audio'::"text"]))),
    CONSTRAINT "media_quality_rules_required_aspect_ratio_check" CHECK ((("required_aspect_ratio" IS NULL) OR ("required_aspect_ratio" ~ '^[0-9]+:[0-9]+$'::"text")))
);


ALTER TABLE "battles"."media_quality_rules" OWNER TO "postgres";


COMMENT ON TABLE "battles"."media_quality_rules" IS 'Phase BK per-(template, modality) quality floors enforced by fn_check_media_quality.';



CREATE TABLE IF NOT EXISTS "battles"."rubric_criteria" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "rubric_id" "uuid" NOT NULL,
    "ordinal" integer NOT NULL,
    "title" "text" NOT NULL,
    "description" "text",
    "weight" numeric DEFAULT 1.0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "rubric_criteria_weight_positive" CHECK (("weight" > (0)::numeric))
);


ALTER TABLE "battles"."rubric_criteria" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."rubrics" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "creator_lenser_id" "uuid" NOT NULL,
    "title" "text" NOT NULL,
    "description" "text",
    "is_public" boolean DEFAULT true NOT NULL,
    "version" integer DEFAULT 1 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "deleted_at" timestamp with time zone
);


ALTER TABLE "battles"."rubrics" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."schedules" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "open_at" timestamp with time zone,
    "judge_at" timestamp with time zone,
    "publish_at" timestamp with time zone,
    "auto_judge" boolean DEFAULT true NOT NULL,
    "auto_publish" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "schedule_judge_before_publish" CHECK ((("judge_at" IS NULL) OR ("publish_at" IS NULL) OR ("judge_at" < "publish_at"))),
    CONSTRAINT "schedule_open_before_judge" CHECK ((("open_at" IS NULL) OR ("judge_at" IS NULL) OR ("open_at" < "judge_at")))
);


ALTER TABLE "battles"."schedules" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."scorecards" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "rubric_criterion_id" "uuid" NOT NULL,
    "result" "battles"."scorecard_result_enum" NOT NULL,
    "scorer_model_id" "uuid",
    "explanation" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "battles"."scorecards" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."scoring_plugin_signals" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "plugin_id" "text" NOT NULL,
    "signals" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "recorded_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "battles"."scoring_plugin_signals" OWNER TO "postgres";


COMMENT ON TABLE "battles"."scoring_plugin_signals" IS 'Phase R2: durable sink for scoring plugin outputs. One row per (battle_id, plugin_id). Written by service_role via public.fn_record_scoring_plugin_signal. Consumed downstream by the finaliser/visualisations; no schema-level dependency from finalize is introduced in this migration.';



CREATE TABLE IF NOT EXISTS "battles"."series_rounds" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "series_id" "uuid" NOT NULL,
    "round_number" integer NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "winner_contender_id" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "series_rounds_round_number_check" CHECK (("round_number" >= 1))
);


ALTER TABLE "battles"."series_rounds" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."sponsorship_contributions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "pool_id" "uuid" NOT NULL,
    "contributor_lenser_id" "uuid" NOT NULL,
    "amount_credits" bigint NOT NULL,
    "external_tx_ref" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "sponsorship_contributions_amount_check" CHECK (("amount_credits" > 0))
);


ALTER TABLE "battles"."sponsorship_contributions" OWNER TO "postgres";


COMMENT ON TABLE "battles"."sponsorship_contributions" IS 'Individual credit deposits into a battle sponsorship pool. external_tx_ref is an opaque wallet transaction ID (not an FK).';



CREATE TABLE IF NOT EXISTS "battles"."sponsorship_payouts" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "pool_id" "uuid" NOT NULL,
    "recipient_lenser_id" "uuid" NOT NULL,
    "amount_credits" bigint NOT NULL,
    "platform_fee_credits" bigint DEFAULT 0 NOT NULL,
    "payout_type" "text" NOT NULL,
    "external_tx_ref" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "sponsorship_payouts_amount_check" CHECK (("amount_credits" > 0)),
    CONSTRAINT "sponsorship_payouts_fee_check" CHECK (("platform_fee_credits" >= 0)),
    CONSTRAINT "sponsorship_payouts_type_check" CHECK (("payout_type" = ANY (ARRAY['winner'::"text", 'runner_up'::"text", 'refund'::"text", 'platform_fee'::"text"])))
);


ALTER TABLE "battles"."sponsorship_payouts" OWNER TO "postgres";


COMMENT ON TABLE "battles"."sponsorship_payouts" IS 'Credit payouts from sponsorship pool after battle closes. Includes winner, runner_up, refund, and platform_fee entries.';



CREATE TABLE IF NOT EXISTS "battles"."sponsorship_pools" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "total_credits" bigint DEFAULT 0 NOT NULL,
    "platform_fee_pct" smallint DEFAULT 10 NOT NULL,
    "status" "battles"."sponsorship_pool_status_enum" DEFAULT 'open'::"battles"."sponsorship_pool_status_enum" NOT NULL,
    "locked_at" timestamp with time zone,
    "distributed_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "payout_mode" "text" DEFAULT 'credits_only'::"text" NOT NULL,
    "legal_jurisdiction" "text",
    CONSTRAINT "ck_battles_sponsorship_pools_total_credits_nonneg" CHECK ((("total_credits" IS NULL) OR ("total_credits" >= 0))),
    CONSTRAINT "sponsorship_payout_mode_check" CHECK (("payout_mode" = ANY (ARRAY['credits_only'::"text", 'fiat_optional'::"text"]))),
    CONSTRAINT "sponsorship_pools_platform_fee_pct_check" CHECK ((("platform_fee_pct" >= 0) AND ("platform_fee_pct" <= 50))),
    CONSTRAINT "sponsorship_pools_total_credits_check" CHECK (("total_credits" >= 0))
);


ALTER TABLE "battles"."sponsorship_pools" OWNER TO "postgres";


COMMENT ON TABLE "battles"."sponsorship_pools" IS 'Credit pool for battle sponsorship. platform_fee_pct is the percentage taken by the platform on distribution. Managed by CF Worker only.';



COMMENT ON COLUMN "battles"."sponsorship_pools"."payout_mode" IS 'credits_only = platform-internal credits only (legally safe default). fiat_optional = requires explicit legal review before enabling. When AI wins a battle, credits go to the human lenser who registered the AI actor — no autonomous AI payout.';



COMMENT ON COLUMN "battles"."sponsorship_pools"."legal_jurisdiction" IS 'Optional ISO 3166-1 alpha-2 country code for the jurisdiction governing this pool. Required before fiat_optional can be enabled.';



CREATE TABLE IF NOT EXISTS "battles"."stream_recordings" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "slot" "text" NOT NULL,
    "events" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    "total_duration_ms" integer NOT NULL,
    "total_tokens" integer DEFAULT 0 NOT NULL,
    "final_output" "text",
    "content_type" "text" DEFAULT 'text'::"text",
    "created_at" timestamp with time zone DEFAULT "now"(),
    CONSTRAINT "ck_battles_stream_recordings_total_duration_ms_nonneg" CHECK ((("total_duration_ms" IS NULL) OR ("total_duration_ms" >= 0))),
    CONSTRAINT "ck_battles_stream_recordings_total_tokens_nonneg" CHECK ((("total_tokens" IS NULL) OR ("total_tokens" >= 0))),
    CONSTRAINT "stream_recordings_slot_check" CHECK (("slot" = ANY (ARRAY['A'::"text", 'B'::"text"])))
);


ALTER TABLE "battles"."stream_recordings" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."template_prompt_variables" (
    "template_id" "uuid" NOT NULL,
    "variable_key" "text" NOT NULL,
    "label" "text" NOT NULL,
    "default_value" "text",
    "required" boolean DEFAULT false NOT NULL,
    "ordinal" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "template_prompt_variables_key_format" CHECK (("variable_key" ~ '^[a-z][a-z0-9_]{0,29}$'::"text")),
    CONSTRAINT "template_prompt_variables_label_check" CHECK ((("char_length"("label") >= 1) AND ("char_length"("label") <= 100)))
);


ALTER TABLE "battles"."template_prompt_variables" OWNER TO "postgres";


COMMENT ON TABLE "battles"."template_prompt_variables" IS 'Phase BN: named slots ({{key}}) that fn_battles_render_prompt substitutes into a template''s task_prompt.';



CREATE TABLE IF NOT EXISTS "battles"."tool_registry" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "display_name" "text" NOT NULL,
    "description" "text",
    "content_type" "text" NOT NULL,
    "config_schema" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tool_registry_content_type_check" CHECK (("content_type" = ANY (ARRAY['text'::"text", 'code'::"text", 'poem'::"text", 'drawing'::"text", 'image'::"text", 'video'::"text", 'audio'::"text", 'workflow'::"text", 'map'::"text", 'avatar'::"text", 'image_edit'::"text", 'kaggle'::"text"])))
);


ALTER TABLE "battles"."tool_registry" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."tournament_matches" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "tournament_id" "uuid" NOT NULL,
    "round_id" "uuid" NOT NULL,
    "battle_id" "uuid",
    "contender_a_id" "uuid",
    "contender_b_id" "uuid",
    "winner_tournament_contender_id" "uuid",
    "next_match_id" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "battles"."tournament_matches" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."tournament_rounds" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "tournament_id" "uuid" NOT NULL,
    "round_number" integer NOT NULL,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tournament_rounds_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'active'::"text", 'completed'::"text"])))
);


ALTER TABLE "battles"."tournament_rounds" OWNER TO "postgres";


CREATE OR REPLACE VIEW "battles"."v_battle_feed_item" AS
 SELECT "b"."id",
    "b"."slug",
    "b"."title",
    "b"."status",
    "b"."battle_type",
    "b"."voter_eligibility",
    "b"."total_vote_count",
    "b"."published_at",
    "b"."voting_opens_at",
    "b"."voting_closes_at",
    "b"."winner_contender_id",
    "b"."created_at",
    "b"."content_type",
    "ca"."id" AS "contender_a_id",
    "ca"."display_name" AS "contender_a_name",
    "ca"."contender_type" AS "contender_a_type",
    "cb"."id" AS "contender_b_id",
    "cb"."display_name" AS "contender_b_name",
    "cb"."contender_type" AS "contender_b_type",
        CASE
            WHEN ("b"."winner_contender_id" = "ca"."id") THEN 'A'::"text"
            WHEN ("b"."winner_contender_id" = "cb"."id") THEN 'B'::"text"
            ELSE NULL::"text"
        END AS "winner_slot"
   FROM (("battles"."battles" "b"
     LEFT JOIN "battles"."contenders" "ca" ON ((("ca"."battle_id" = "b"."id") AND ("ca"."slot" = 'A'::"bpchar") AND ("ca"."contender_status" = 'active'::"text"))))
     LEFT JOIN "battles"."contenders" "cb" ON ((("cb"."battle_id" = "b"."id") AND ("cb"."slot" = 'B'::"bpchar") AND ("cb"."contender_status" = 'active'::"text"))))
  WHERE (("b"."deleted_at" IS NULL) AND ("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"])));


ALTER VIEW "battles"."v_battle_feed_item" OWNER TO "postgres";


COMMENT ON VIEW "battles"."v_battle_feed_item" IS 'Denormalized battle card for BattlesFeedPage. Used by fn_get_battles_feed. Includes contender names, winner_slot, and content_type for media-type badges. Only exposes post-draft statuses (voting, scoring, closed, published, archived).';



CREATE TABLE IF NOT EXISTS "execution"."runs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "request_id" "uuid" NOT NULL,
    "status" "text" DEFAULT 'queued'::"text" NOT NULL,
    "model_id" "uuid",
    "provider_request_id" "text",
    "execution_hash" "text",
    "input_hash" "text",
    "output_hash" "text",
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "latency_ms" integer,
    "error_code" "text",
    "error_message" "text",
    "cost_estimate" numeric,
    "token_input" integer,
    "token_output" integer,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "credit_cost" bigint,
    "billing_status" "text" DEFAULT 'free'::"text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "response_text" "text",
    "response_meta" "jsonb" DEFAULT '{}'::"jsonb",
    "throttle_applied" boolean DEFAULT false NOT NULL,
    "injected_delay_ms" integer DEFAULT 0 NOT NULL,
    "is_async" boolean DEFAULT false NOT NULL,
    "provider_task_id" "text",
    "last_polled_at" timestamp with time zone,
    CONSTRAINT "runs_billing_status_check" CHECK (("billing_status" = ANY (ARRAY['pending'::"text", 'charged'::"text", 'failed'::"text", 'free'::"text"]))),
    CONSTRAINT "runs_status_check" CHECK (("status" = ANY (ARRAY['queued'::"text", 'running'::"text", 'succeeded'::"text", 'failed'::"text", 'canceled'::"text", 'timed_out'::"text"]))),
    CONSTRAINT "runs_timestamp_order" CHECK ((("completed_at" IS NULL) OR ("started_at" IS NULL) OR ("completed_at" >= "started_at")))
);


ALTER TABLE "execution"."runs" OWNER TO "postgres";


COMMENT ON COLUMN "execution"."runs"."credit_cost" IS 'Platform credits charged for this run. NULL = not billed. Managed by CF Worker (service_role).';



COMMENT ON COLUMN "execution"."runs"."billing_status" IS 'Billing lifecycle: free (default/self-hosted), pending (charge initiated), charged (wallet debited), failed (insufficient balance).';



COMMENT ON COLUMN "execution"."runs"."is_active" IS 'Soft-delete flag. Set to false instead of hard-deleting. Queries should filter WHERE is_active = true by default.';



COMMENT ON COLUMN "execution"."runs"."response_text" IS 'Primary AI response content. Written by the API Worker after provider call completes. NULL for in-progress or failed runs.';



COMMENT ON COLUMN "execution"."runs"."response_meta" IS 'Provider response metadata: model name, finish_reason, provider_request_id, usage stats, etc. Structured as JSONB for provider flexibility.';



COMMENT ON COLUMN "execution"."runs"."throttle_applied" IS 'True if the CF Worker applied a token-rate cap, time deadline, or delay handicap during this run.';



COMMENT ON COLUMN "execution"."runs"."injected_delay_ms" IS 'Actual milliseconds of artificial delay injected by the CF Worker before streaming began.';



COMMENT ON COLUMN "execution"."runs"."is_async" IS 'TRUE for video/audio providers that return a task ID and require polling.';



COMMENT ON COLUMN "execution"."runs"."provider_task_id" IS 'Provider-specific task/job ID returned by async generation APIs (Sora, Veo, Kling, etc).';



COMMENT ON COLUMN "execution"."runs"."last_polled_at" IS 'Set by the async-media-poll-worker each time the provider status endpoint is queried. NULL means the run has not been polled yet. Used for SKIP LOCKED ordering so concurrent workers do not double-poll the same run.';



CREATE OR REPLACE VIEW "battles"."v_benchmark_dataset" AS
 SELECT "b"."id" AS "battle_id",
    "b"."title" AS "battle_title",
    "b"."task_source",
    "b"."challenge_type",
    "b"."contender_structure",
    "b"."judging_mode",
    ("b"."benchmark_config" ->> 'game_key'::"text") AS "benchmark_game_key",
    ("b"."benchmark_config" ->> 'dataset_schema_version'::"text") AS "dataset_schema_version",
    "b"."status" AS "battle_status",
    "b"."created_at" AS "battle_created_at",
    "gc"."id" AS "challenge_id",
    "gc"."challenge_type" AS "challenge_game_type",
    "gc"."content_hash" AS "challenge_hash",
    "gc"."difficulty",
    "gc"."language",
    "gc"."time_limit_seconds",
    "gc"."scoring_mode",
    "gc"."input_snapshot" AS "challenge_input_params",
    "gc"."question_text" AS "challenge_question",
    "gc"."question_payload" AS "challenge_payload",
    "gc"."answer_key_hash",
    "gc"."generator_lens_id",
    "gc"."generator_version_id",
    "gc"."execution_run_id" AS "generator_run_id",
    "gc"."locked_at" AS "challenge_locked_at",
    "b"."shared_input_snapshot",
    "co"."id" AS "contender_id",
    "co"."slot" AS "contender_slot",
    "co"."contender_type",
    "s"."id" AS "submission_id",
    "s"."integrity_hash" AS "submission_hash",
    "s"."output_modality",
    "s"."submitted_at",
    "s"."execution_run_id" AS "contender_run_id",
    "er"."latency_ms",
    "er"."token_input",
    "er"."token_output",
    "er"."credit_cost",
        CASE
            WHEN (("b"."benchmark_config" ->> 'anonymization_policy'::"text") = 'full'::"text") THEN NULL::"text"
            ELSE ("er"."model_id")::"text"
        END AS "model_id",
    "b"."winner_contender_id",
    "b"."normalized_score",
    "ajv"."score" AS "ai_judge_score",
        CASE
            WHEN (("b"."benchmark_config" ->> 'anonymization_policy'::"text") = 'full'::"text") THEN NULL::"text"
            ELSE "ajv"."model_key"
        END AS "ai_judge_model_key",
    ("b"."benchmark_config" ->> 'anonymization_policy'::"text") AS "anonymization_policy"
   FROM ((((("battles"."battles" "b"
     LEFT JOIN "battles"."generated_challenges" "gc" ON (("gc"."id" = "b"."generated_challenge_id")))
     LEFT JOIN "battles"."contenders" "co" ON (("co"."battle_id" = "b"."id")))
     LEFT JOIN "battles"."submissions" "s" ON ((("s"."battle_id" = "b"."id") AND ("s"."contender_id" = "co"."id") AND ("s"."is_final" = true))))
     LEFT JOIN "execution"."runs" "er" ON (("er"."id" = "s"."execution_run_id")))
     LEFT JOIN "battles"."ai_judge_verdicts" "ajv" ON ((("ajv"."battle_id" = "b"."id") AND ("ajv"."contender_id" = "co"."id"))))
  WHERE (((("b"."benchmark_config" ->> 'export_eligible'::"text"))::boolean IS TRUE) AND ("b"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL));


ALTER VIEW "battles"."v_benchmark_dataset" OWNER TO "postgres";


COMMENT ON VIEW "battles"."v_benchmark_dataset" IS 'Safe benchmark dataset export view. Filters to export_eligible=true battles in closed/published/archived state. Masks model_id and ai_judge_model_key when anonymization_policy=full. Never exposes answer_key_encrypted, API keys, private instructions, or BYOK references.';



CREATE OR REPLACE VIEW "battles"."v_execution_jobs_public" AS
 SELECT "id",
    "battle_id",
    "slot",
    "status",
    "claimed_at",
    "completed_at",
    "retry_count",
    "created_at"
   FROM "battles"."battle_execution_jobs" "j";


ALTER VIEW "battles"."v_execution_jobs_public" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."vote_choices" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "vote_id" "uuid" NOT NULL,
    "contender_id" "uuid" NOT NULL,
    "rank_ordinal" integer,
    "score_value" numeric,
    "is_selected" boolean DEFAULT false NOT NULL,
    "rationale" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "battles"."vote_choices" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "battles"."votes" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "battle_id" "uuid" NOT NULL,
    "voter_lenser_id" "uuid" NOT NULL,
    "vote_value" "battles"."vote_value_enum" NOT NULL,
    "rationale" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "voted_contender_id" "uuid",
    "is_draw" boolean DEFAULT false NOT NULL,
    "weight" numeric DEFAULT 1.0 NOT NULL,
    "is_ai_vote" boolean DEFAULT false NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "battles"."votes" OWNER TO "postgres";


COMMENT ON TABLE "battles"."votes" IS 'Vote records. Direct INSERT restricted to service_role. Authenticated users must use public.fn_submit_vote (SECURITY DEFINER) which atomically updates vote_aggregates and total_vote_count.';



CREATE TABLE IF NOT EXISTS "billing"."budget_policies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "scope" "text" NOT NULL,
    "scope_id" "uuid" NOT NULL,
    "daily_credits" numeric(20,6),
    "monthly_credits" numeric(20,6),
    "burst_credits" numeric(20,6),
    "soft_threshold_pct" smallint DEFAULT 80 NOT NULL,
    "hard_action" "text" DEFAULT 'block'::"text" NOT NULL,
    "rpm_limit" integer,
    "tpm_limit" integer,
    "emergency_kill" boolean DEFAULT false NOT NULL,
    "updated_by" "uuid",
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "budget_policies_hard_action_check" CHECK (("hard_action" = ANY (ARRAY['block'::"text", 'queue'::"text", 'degrade'::"text"]))),
    CONSTRAINT "budget_policies_rpm_limit_check" CHECK ((("rpm_limit" IS NULL) OR ("rpm_limit" > 0))),
    CONSTRAINT "budget_policies_scope_check" CHECK (("scope" = ANY (ARRAY['agent'::"text", 'org'::"text", 'user'::"text"]))),
    CONSTRAINT "budget_policies_soft_threshold_pct_check" CHECK ((("soft_threshold_pct" >= 1) AND ("soft_threshold_pct" <= 99))),
    CONSTRAINT "budget_policies_tpm_limit_check" CHECK ((("tpm_limit" IS NULL) OR ("tpm_limit" > 0)))
);


ALTER TABLE "billing"."budget_policies" OWNER TO "postgres";


COMMENT ON TABLE "billing"."budget_policies" IS 'CG1: surface only; CG2 wires fn_cost_reserve to consult this table. In CG1, fn_cost_reserve falls back to agents.policies.spending_limit_credits.';



CREATE TABLE IF NOT EXISTS "billing"."checkout_sessions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "variant_id" "uuid" NOT NULL,
    "ls_checkout_url" "text",
    "custom_price_cents" bigint,
    "status" "text" DEFAULT 'created'::"text" NOT NULL,
    "expires_at" timestamp with time zone,
    "test_mode" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "completed_at" timestamp with time zone,
    CONSTRAINT "checkout_sessions_custom_price_check" CHECK ((("custom_price_cents" IS NULL) OR ("custom_price_cents" > 0))),
    CONSTRAINT "checkout_sessions_status_check" CHECK (("status" = ANY (ARRAY['created'::"text", 'completed'::"text", 'expired'::"text", 'abandoned'::"text"])))
);


ALTER TABLE "billing"."checkout_sessions" OWNER TO "postgres";


COMMENT ON TABLE "billing"."checkout_sessions" IS 'Tracks LemonSqueezy checkout creation. Maps lenser_id to checkout URL. Status updated when webhook confirms payment.';



CREATE TABLE IF NOT EXISTS "billing"."cost_reservations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "actor_id" "uuid",
    "org_id" "uuid",
    "model_id" "uuid" NOT NULL,
    "provider_key" "text" NOT NULL,
    "pricing_snapshot_id" "uuid" NOT NULL,
    "reserved_credits" numeric(20,6) NOT NULL,
    "reserved_usd" numeric(20,6) NOT NULL,
    "running_credits" numeric(20,6) DEFAULT 0 NOT NULL,
    "committed_credits" numeric(20,6),
    "committed_usd" numeric(20,6),
    "status" "text" DEFAULT 'held'::"text" NOT NULL,
    "reason_released" "text",
    "context" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "idempotency_key" "text" NOT NULL,
    "shadow_mode" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "held_until" timestamp with time zone NOT NULL,
    "committed_at" timestamp with time zone,
    "released_at" timestamp with time zone,
    CONSTRAINT "cost_reservations_committed_credits_check" CHECK ((("committed_credits" IS NULL) OR ("committed_credits" >= (0)::numeric))),
    CONSTRAINT "cost_reservations_held_future" CHECK ((("status" <> 'held'::"text") OR ("held_until" > "created_at"))),
    CONSTRAINT "cost_reservations_reserved_credits_check" CHECK (("reserved_credits" > (0)::numeric)),
    CONSTRAINT "cost_reservations_reserved_usd_check" CHECK (("reserved_usd" >= (0)::numeric)),
    CONSTRAINT "cost_reservations_running_credits_check" CHECK (("running_credits" >= (0)::numeric)),
    CONSTRAINT "cost_reservations_status_check" CHECK (("status" = ANY (ARRAY['held'::"text", 'committed'::"text", 'released'::"text", 'expired'::"text"])))
);


ALTER TABLE "billing"."cost_reservations" OWNER TO "postgres";


COMMENT ON TABLE "billing"."cost_reservations" IS 'CG1: escrow row for one upcoming AI provider call. Lifecycle: held -> committed | released | expired. The id is the unforgeable per-call token consumed by fn_byok_key_resolve_v2.';



CREATE TABLE IF NOT EXISTS "billing"."execution_margin_policies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "model_id" "uuid",
    "markup_percent" numeric(5,2) DEFAULT 0 NOT NULL,
    "fixed_fee_usd" numeric(12,8) DEFAULT 0 NOT NULL,
    "rounding_mode" "text" DEFAULT 'ceil'::"text" NOT NULL,
    "min_charge_credits" integer DEFAULT 1 NOT NULL,
    "max_charge_credits" integer,
    "effective_from" timestamp with time zone DEFAULT "now"() NOT NULL,
    "effective_to" timestamp with time zone,
    "is_active" boolean DEFAULT true NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "billing_margin_policies_markup_nonneg" CHECK ((("markup_percent" >= (0)::numeric) AND ("fixed_fee_usd" >= (0)::numeric))),
    CONSTRAINT "billing_margin_policies_max_gte_min" CHECK ((("max_charge_credits" IS NULL) OR ("max_charge_credits" >= "min_charge_credits"))),
    CONSTRAINT "billing_margin_policies_min_credits_nonneg" CHECK (("min_charge_credits" >= 0)),
    CONSTRAINT "billing_margin_policies_rounding_check" CHECK (("rounding_mode" = ANY (ARRAY['ceil'::"text", 'floor'::"text", 'round'::"text"])))
);


ALTER TABLE "billing"."execution_margin_policies" OWNER TO "postgres";


COMMENT ON TABLE "billing"."execution_margin_policies" IS 'Platform markup policies applied on top of AI provider cost. model_id=NULL = global default policy. SERVICE_ROLE ONLY. Cross-schema FK to ai.models(id) — billing schema depends on ai schema for model identity.';



CREATE TABLE IF NOT EXISTS "billing"."ledger_entries" (
    "id" bigint NOT NULL,
    "reservation_id" "uuid" NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "org_id" "uuid",
    "direction" "text" NOT NULL,
    "amount_credits" numeric(20,6) NOT NULL,
    "amount_usd" numeric(20,6) NOT NULL,
    "prev_hash" "bytea",
    "entry_hash" "bytea" NOT NULL,
    "recorded_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ledger_entries_direction_check" CHECK (("direction" = ANY (ARRAY['debit_hold'::"text", 'debit_commit'::"text", 'release'::"text", 'adjustment'::"text"])))
);


ALTER TABLE "billing"."ledger_entries" OWNER TO "postgres";


COMMENT ON TABLE "billing"."ledger_entries" IS 'CG1: append-only, hash-chained ledger of all credit movements. Triggers compute entry_hash and block UPDATE/DELETE; an offline reconciliation job recomputes the chain.';



CREATE SEQUENCE IF NOT EXISTS "billing"."ledger_entries_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "billing"."ledger_entries_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "billing"."ledger_entries_id_seq" OWNED BY "billing"."ledger_entries"."id";



CREATE TABLE IF NOT EXISTS "billing"."orders" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ls_order_id" bigint NOT NULL,
    "ls_identifier" "text" NOT NULL,
    "webhook_event_id" "uuid" NOT NULL,
    "store_id" bigint NOT NULL,
    "ls_customer_id" bigint NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "order_number" integer NOT NULL,
    "user_name" "text",
    "user_email" "text" NOT NULL,
    "currency" "text" DEFAULT 'USD'::"text" NOT NULL,
    "currency_rate" numeric(10,6) DEFAULT 1.0 NOT NULL,
    "status" "billing"."order_status_enum" DEFAULT 'pending'::"billing"."order_status_enum" NOT NULL,
    "subtotal_cents" bigint NOT NULL,
    "tax_cents" bigint DEFAULT 0 NOT NULL,
    "total_cents" bigint NOT NULL,
    "total_usd_cents" bigint NOT NULL,
    "product_id" "uuid",
    "variant_id" "uuid",
    "product_name" "text" NOT NULL,
    "variant_name" "text",
    "credits_granted" bigint DEFAULT 0 NOT NULL,
    "first_order" boolean DEFAULT false NOT NULL,
    "test_mode" boolean DEFAULT false NOT NULL,
    "refunded" boolean DEFAULT false NOT NULL,
    "refunded_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_billing_orders_total_cents_nonneg" CHECK ((("total_cents" IS NULL) OR ("total_cents" >= 0))),
    CONSTRAINT "ck_billing_orders_total_usd_cents_nonneg" CHECK ((("total_usd_cents" IS NULL) OR ("total_usd_cents" >= 0)))
);


ALTER TABLE "billing"."orders" OWNER TO "postgres";


COMMENT ON TABLE "billing"."orders" IS 'Processed LemonSqueezy orders mapped to lenser_id. lenser_id resolved from checkout_data.custom.user_id in webhook payload.';



COMMENT ON COLUMN "billing"."orders"."ls_identifier" IS 'LemonSqueezy UUID identifier for the order. Useful as external reference.';



COMMENT ON COLUMN "billing"."orders"."lenser_id" IS 'Resolved from checkout_data.custom.user_id. ON DELETE RESTRICT — cannot delete profile with paid orders.';



COMMENT ON COLUMN "billing"."orders"."credits_granted" IS 'Credits credited to wallet from this order. Sourced from billing.products.credits_granted.';



CREATE TABLE IF NOT EXISTS "billing"."pricing_snapshots" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "model_id" "uuid" NOT NULL,
    "source_pricing_id" "uuid" NOT NULL,
    "unit_type" "ai"."unit_type_enum" NOT NULL,
    "input_cpm_usd" numeric(20,10) NOT NULL,
    "output_cpm_usd" numeric(20,10) NOT NULL,
    "unit_cost_usd" numeric(20,10),
    "credit_rate_usd" numeric(20,10) DEFAULT 0.001 NOT NULL,
    "taken_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "pricing_snapshots_credit_rate_usd_check" CHECK (("credit_rate_usd" > (0)::numeric)),
    CONSTRAINT "pricing_snapshots_input_cpm_usd_check" CHECK (("input_cpm_usd" >= (0)::numeric)),
    CONSTRAINT "pricing_snapshots_output_cpm_usd_check" CHECK (("output_cpm_usd" >= (0)::numeric))
);


ALTER TABLE "billing"."pricing_snapshots" OWNER TO "postgres";


COMMENT ON TABLE "billing"."pricing_snapshots" IS 'CG1: immutable pin of provider list price at quote time. References the authoritative ai.model_pricing row so a price change between quote and commit cannot create or hide spend.';



COMMENT ON COLUMN "billing"."pricing_snapshots"."credit_rate_usd" IS 'USD per platform credit at snapshot time. Default 0.001 = 1000 credits/$. Per-org overrides land in CG8 via tenancy.orgs.credit_rate_usd_override.';



CREATE TABLE IF NOT EXISTS "billing"."products" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ls_product_id" bigint NOT NULL,
    "store_id" bigint NOT NULL,
    "name" "text" NOT NULL,
    "slug" "text" NOT NULL,
    "status" "billing"."product_status_enum" DEFAULT 'draft'::"billing"."product_status_enum" NOT NULL,
    "price_cents" bigint NOT NULL,
    "credits_granted" bigint NOT NULL,
    "pay_what_you_want" boolean DEFAULT false NOT NULL,
    "buy_now_url" "text",
    "description" "text",
    "test_mode" boolean DEFAULT false NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "products_credits_granted_check" CHECK (("credits_granted" > 0)),
    CONSTRAINT "products_price_cents_check" CHECK (("price_cents" >= 0))
);


ALTER TABLE "billing"."products" OWNER TO "postgres";


COMMENT ON TABLE "billing"."products" IS 'LemonSqueezy credit pack products. 1 credit = $0.01 USD (100 credits per USD).';



COMMENT ON COLUMN "billing"."products"."ls_product_id" IS 'LemonSqueezy product ID (integer). Used to match webhook payloads.';



COMMENT ON COLUMN "billing"."products"."credits_granted" IS 'Number of wallet credits granted when this product is purchased.';



CREATE TABLE IF NOT EXISTS "billing"."provider_circuit_state" (
    "provider_key" "text" NOT NULL,
    "state" "text" DEFAULT 'closed'::"text" NOT NULL,
    "failure_count" integer DEFAULT 0 NOT NULL,
    "last_failure_at" timestamp with time zone,
    "opened_at" timestamp with time zone,
    "next_probe_at" timestamp with time zone,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "provider_circuit_state_failure_count_check" CHECK (("failure_count" >= 0)),
    CONSTRAINT "provider_circuit_state_state_check" CHECK (("state" = ANY (ARRAY['closed'::"text", 'half_open'::"text", 'open'::"text"])))
);


ALTER TABLE "billing"."provider_circuit_state" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "billing"."runtime_settings" (
    "id" smallint DEFAULT 1 NOT NULL,
    "enforce" boolean DEFAULT false NOT NULL,
    "byok_require_reservation" boolean DEFAULT false NOT NULL,
    "reservation_default_ttl" interval DEFAULT '00:05:00'::interval NOT NULL,
    "meter_headroom_pct" smallint DEFAULT 10 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "runtime_settings_id_check" CHECK (("id" = 1)),
    CONSTRAINT "runtime_settings_meter_headroom_pct_check" CHECK ((("meter_headroom_pct" >= 0) AND ("meter_headroom_pct" <= 100)))
);


ALTER TABLE "billing"."runtime_settings" OWNER TO "postgres";


COMMENT ON TABLE "billing"."runtime_settings" IS 'CG1: singleton runtime config for the cost governance plane. id=1 enforced.';



COMMENT ON COLUMN "billing"."runtime_settings"."enforce" IS 'When false, fn_cost_reserve still records reservations and ledger entries but does NOT raise on budget violations (shadow mode). Flip to true after CG2 ships budget policies. Mirrors the BILLING_ENFORCE env flag on workers.';



COMMENT ON COLUMN "billing"."runtime_settings"."byok_require_reservation" IS 'When true, legacy fn_byok_key_resolve (v1) raises E_BYOK_CONTEXT_MISSING; callers must use fn_byok_key_resolve_v2 with a held reservation_id. Keep false until every worker (lf-gatewayd, platform-api) ships v2 calls.';



CREATE TABLE IF NOT EXISTS "billing"."spend_anomalies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "window_start" timestamp with time zone NOT NULL,
    "window_end" timestamp with time zone NOT NULL,
    "score" numeric(6,3) NOT NULL,
    "signals" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "action_taken" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "spend_anomalies_check" CHECK (("window_end" > "window_start"))
);


ALTER TABLE "billing"."spend_anomalies" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "billing"."variants" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ls_variant_id" bigint NOT NULL,
    "product_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "slug" "text" NOT NULL,
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    "has_license_keys" boolean DEFAULT false NOT NULL,
    "test_mode" boolean DEFAULT false NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "variants_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'inactive'::"text"])))
);


ALTER TABLE "billing"."variants" OWNER TO "postgres";


COMMENT ON TABLE "billing"."variants" IS 'Mirror of LemonSqueezy variants. Each product has at least one variant.';



CREATE OR REPLACE VIEW "billing"."vw_products" AS
 SELECT "p"."id",
    "p"."ls_product_id",
    "p"."name",
    "p"."slug",
    "p"."description",
    "p"."price_cents",
    "p"."credits_granted",
    "p"."pay_what_you_want",
    "p"."buy_now_url",
    "p"."test_mode",
    "p"."metadata",
    "v"."id" AS "variant_id",
    "v"."ls_variant_id",
    "v"."name" AS "variant_name",
    COALESCE("o"."order_count", (0)::bigint) AS "order_count"
   FROM (("billing"."products" "p"
     JOIN LATERAL ( SELECT "variants"."id",
            "variants"."ls_variant_id",
            "variants"."name"
           FROM "billing"."variants"
          WHERE (("variants"."product_id" = "p"."id") AND ("variants"."status" = 'active'::"text"))
          ORDER BY "variants"."ls_variant_id"
         LIMIT 1) "v" ON (true))
     LEFT JOIN ( SELECT "orders"."product_id",
            "count"(*) AS "order_count"
           FROM "billing"."orders"
          WHERE ("orders"."status" = 'paid'::"billing"."order_status_enum")
          GROUP BY "orders"."product_id") "o" ON (("o"."product_id" = "p"."id")))
  WHERE ("p"."status" = 'published'::"billing"."product_status_enum")
  ORDER BY "p"."price_cents";


ALTER VIEW "billing"."vw_products" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "billing"."webhook_events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ls_event_id" "text" NOT NULL,
    "event_name" "text" NOT NULL,
    "payload" "jsonb" NOT NULL,
    "processed" boolean DEFAULT false NOT NULL,
    "processed_at" timestamp with time zone,
    "error_message" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "billing"."webhook_events" OWNER TO "postgres";


COMMENT ON TABLE "billing"."webhook_events" IS 'Raw LemonSqueezy webhook event log. ls_event_id is the idempotency key — duplicate webhooks are rejected by UNIQUE constraint.';



COMMENT ON COLUMN "billing"."webhook_events"."payload" IS 'Full JSON:API envelope from LemonSqueezy webhook.';



CREATE TABLE IF NOT EXISTS "connectors"."connector_tokens" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "connector_id" "uuid" NOT NULL,
    "token_hash" "text" NOT NULL,
    "token_prefix" "text" NOT NULL,
    "scopes" "text"[] NOT NULL,
    "revoked_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "connector_tokens_scopes_nonempty" CHECK (("cardinality"("scopes") > 0))
);


ALTER TABLE "connectors"."connector_tokens" OWNER TO "postgres";


COMMENT ON TABLE "connectors"."connector_tokens" IS 'Hashed service tokens for connectors. Raw tokens are returned only once; lookup uses token_hash = sha256(token).';



CREATE TABLE IF NOT EXISTS "connectors"."connectors" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "slug" "text" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "kind" "text" DEFAULT 'api'::"text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_by" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "last_used_at" timestamp with time zone,
    CONSTRAINT "connectors_kind_check" CHECK (("kind" = ANY (ARRAY['api'::"text", 'webhook'::"text"]))),
    CONSTRAINT "connectors_slug_format" CHECK ((("slug" ~ '^[a-z0-9]+([_-][a-z0-9]+)*$'::"text") AND (("char_length"("slug") >= 3) AND ("char_length"("slug") <= 64))))
);


ALTER TABLE "connectors"."connectors" OWNER TO "postgres";


COMMENT ON TABLE "connectors"."connectors" IS 'Workspace-scoped service connectors. One row per registered integration; tokens stored separately.';



CREATE TABLE IF NOT EXISTS "content"."entity_translations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "entity_type" "content"."entity_type_enum" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "language_code" "text" NOT NULL,
    "title" "text",
    "description" "text",
    "content" "text",
    "is_original" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_content_entity_translations_content_len" CHECK ((("content" IS NULL) OR ("length"("content") <= 32000))),
    CONSTRAINT "ck_content_entity_translations_description_len" CHECK ((("description" IS NULL) OR ("length"("description") <= 2000))),
    CONSTRAINT "ck_content_entity_translations_language_code_bcp47" CHECK ("i18n"."fn_is_supported_language"("language_code")),
    CONSTRAINT "ck_content_entity_translations_title_len" CHECK ((("title" IS NULL) OR ("length"("title") <= 200)))
);


ALTER TABLE "content"."entity_translations" OWNER TO "postgres";


COMMENT ON TABLE "content"."entity_translations" IS 'Unified polymorphic translation table. Replaces content.lens_translations and content.thread_translations. entity_type discriminates the translated entity.';



COMMENT ON COLUMN "content"."entity_translations"."description" IS 'Nullable. Populated for prompt_template entities only.';



CREATE TABLE IF NOT EXISTS "content"."reactions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "entity_type" "content"."entity_type_enum" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "reaction" "content"."reaction_enum" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "content"."reactions" OWNER TO "postgres";


COMMENT ON TABLE "content"."reactions" IS 'Unified polymorphic reaction log. Replaces content.lens_reactions, content.thread_reactions, and content.thread_reply_reactions. entity_type discriminates the target entity.';



COMMENT ON COLUMN "content"."reactions"."entity_type" IS 'Discriminates the target entity: ''lens'', ''thread'', or ''thread_reply''.';



COMMENT ON COLUMN "content"."reactions"."entity_id" IS 'UUID of the reacted-to entity. No FK declared (polymorphic target).';



COMMENT ON COLUMN "content"."reactions"."lenser_id" IS 'The lenser who reacted. Populated from auth.uid() via trigger or RPC.';



CREATE TABLE IF NOT EXISTS "content"."reports" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "reporter_id" "uuid" NOT NULL,
    "target_type" "content"."entity_type_enum" NOT NULL,
    "target_id" "uuid" NOT NULL,
    "reason" "content"."report_reason_enum" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "content"."reports" OWNER TO "postgres";


COMMENT ON COLUMN "content"."reports"."reason" IS 'Reason for the report. Typed as content.report_reason_enum.';



CREATE TABLE IF NOT EXISTS "content"."tag_map" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "entity_type" "content"."entity_type_enum" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "tag_id" "uuid" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "content"."tag_map" OWNER TO "postgres";


COMMENT ON TABLE "content"."tag_map" IS 'Records which tags are applied to which entities. Tag actor and client metadata moved to audit.events payload (event_type=''tag_applied'').';



CREATE TABLE IF NOT EXISTS "content"."tag_suggestions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "entity_type" "content"."entity_type_enum" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "tag_id" "uuid" NOT NULL,
    "confidence_score" numeric NOT NULL,
    "ai_model_id" "uuid",
    "status" "content"."suggestion_status_enum" DEFAULT 'pending'::"content"."suggestion_status_enum",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_content_tag_suggestions_confidence_score_nonneg" CHECK ((("confidence_score" IS NULL) OR ("confidence_score" >= (0)::numeric))),
    CONSTRAINT "tag_suggestions_confidence_score_check" CHECK ((("confidence_score" >= (0)::numeric) AND ("confidence_score" <= (1)::numeric)))
);


ALTER TABLE "content"."tag_suggestions" OWNER TO "postgres";


COMMENT ON COLUMN "content"."tag_suggestions"."ai_model_id" IS 'The AI model that produced this tag suggestion. NULL = suggestion source is unknown or model was deleted. FK to ai.models(id).';



COMMENT ON COLUMN "content"."tag_suggestions"."status" IS 'Review status of this AI tag suggestion. Typed as content.suggestion_status_enum.';



CREATE TABLE IF NOT EXISTS "content"."tag_translations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "tag_id" "uuid" NOT NULL,
    "language_code" "text" NOT NULL,
    "name" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_content_tag_translations_language_code_bcp47" CHECK ("i18n"."fn_is_supported_language"("language_code"))
);


ALTER TABLE "content"."tag_translations" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "content"."tags" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "visibility" "content"."tag_visibility_enum" DEFAULT 'public'::"content"."tag_visibility_enum" NOT NULL,
    CONSTRAINT "tags_slug_format_check" CHECK ((("slug" ~ '^[a-z0-9]+([\-][a-z0-9]+)*$'::"text") AND (("char_length"("slug") >= 2) AND ("char_length"("slug") <= 40))))
);


ALTER TABLE "content"."tags" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "content"."threads" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" DEFAULT "lensers"."get_auth_lenser_id"() NOT NULL,
    "visibility" "content"."visibility_enum" DEFAULT 'public'::"content"."visibility_enum" NOT NULL,
    "view_count" integer DEFAULT 0 NOT NULL,
    "reply_count" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "thumbnail_url" "text",
    "status" "content"."content_status" DEFAULT 'published'::"content"."content_status" NOT NULL,
    "lens_data" "jsonb",
    "linked_lens_id" "uuid",
    CONSTRAINT "ck_content_threads_reply_count_nonneg" CHECK ((("reply_count" IS NULL) OR ("reply_count" >= 0))),
    CONSTRAINT "ck_content_threads_view_count_nonneg" CHECK ((("view_count" IS NULL) OR ("view_count" >= 0)))
);


ALTER TABLE "content"."threads" OWNER TO "postgres";


COMMENT ON COLUMN "content"."threads"."lens_data" IS 'DEPRECATED: Use linked_lens_id FK instead. Will be dropped in a future migration.';



COMMENT ON COLUMN "content"."threads"."linked_lens_id" IS 'FK to the prompt template this thread was created from. Replaces the lens_data jsonb column. NULL = thread was not created from a prompt, or the prompt has been deleted.';



CREATE TABLE IF NOT EXISTS "lenses"."lenses" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" DEFAULT "lensers"."get_auth_lenser_id"() NOT NULL,
    "visibility" "content"."visibility_enum" DEFAULT 'public'::"content"."visibility_enum" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "status" "content"."content_status" DEFAULT 'published'::"content"."content_status" NOT NULL,
    "parent_lens_id" "uuid",
    "forked_from_execution_id" "uuid",
    "head_version_id" "uuid",
    "forked_from_version_id" "uuid",
    "is_featured" boolean DEFAULT false NOT NULL,
    "featured_at" timestamp with time zone,
    "archived_at" timestamp with time zone,
    "deleted_at" timestamp with time zone
);

ALTER TABLE ONLY "lenses"."lenses" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."lenses" OWNER TO "postgres";


COMMENT ON COLUMN "lenses"."lenses"."parent_lens_id" IS 'Fork lineage: points to the prompt this was derived from. NULL = root prompt.';



COMMENT ON COLUMN "lenses"."lenses"."forked_from_execution_id" IS 'Execution run whose primary artifact content seeded this fork. NULL = manual fork or root.';



COMMENT ON COLUMN "lenses"."lenses"."head_version_id" IS 'Git-style HEAD pointer. Set to the initial draft on lens creation. Advances to each newly published version. NULL only for lenses that have no versions yet.';



COMMENT ON COLUMN "lenses"."lenses"."forked_from_version_id" IS 'The specific lenses.versions row that this lens was cloned from.
   NULL when the lens is a root (not a fork) or was cloned before this column was added.
   Distinct from parent_lens_id (which tracks the parent lens asset, not a specific version).';



CREATE OR REPLACE VIEW "content"."vw_auth_lenses" AS
 SELECT "p"."id",
    "p"."lenser_id",
    "p"."visibility",
    "p"."status",
    "p"."created_at",
    "p"."updated_at",
    "pt"."title",
    "pt"."description",
    "pt"."content",
    "pt"."language_code",
    "rt"."reaction_totals",
    "jsonb_build_object"('handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile"
   FROM ((("lenses"."lenses" "p"
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "p"."lenser_id")))
     LEFT JOIN "content"."entity_translations" "pt" ON ((("pt"."entity_id" = "p"."id") AND ("pt"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("pt"."is_original" = true))))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals"
           FROM ( SELECT "r"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "r"
                  WHERE (("r"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("r"."entity_id" = "p"."id"))
                  GROUP BY "r"."reaction") "x") "rt" ON (true));


ALTER VIEW "content"."vw_auth_lenses" OWNER TO "postgres";


CREATE OR REPLACE VIEW "content"."vw_auth_threads" AS
 SELECT "t"."id",
    "t"."lenser_id",
    "t"."visibility",
    "t"."status",
    "t"."view_count",
    "t"."reply_count",
    "t"."thumbnail_url",
    "t"."created_at",
    "t"."updated_at",
    "t"."lens_data",
    "tt"."title",
    "tt"."content",
    "tt"."language_code",
    "rt"."reaction_totals",
    "jsonb_build_object"('handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile"
   FROM ((("content"."threads" "t"
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "t"."lenser_id")))
     LEFT JOIN "content"."entity_translations" "tt" ON ((("tt"."entity_id" = "t"."id") AND ("tt"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("tt"."is_original" = true))))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals"
           FROM ( SELECT "r"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "r"
                  WHERE (("r"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("r"."entity_id" = "t"."id"))
                  GROUP BY "r"."reaction") "x") "rt" ON (true));


ALTER VIEW "content"."vw_auth_threads" OWNER TO "postgres";


CREATE OR REPLACE VIEW "content"."vw_tag_cross_lang" AS
 SELECT DISTINCT "tt1"."tag_id" AS "source_tag_id",
    "tt2"."tag_id" AS "equivalent_tag_id"
   FROM ((("content"."tag_translations" "tt1"
     JOIN "content"."tag_translations" "tt2" ON ((("lower"(TRIM(BOTH FROM "tt1"."name")) = "lower"(TRIM(BOTH FROM "tt2"."name"))) AND ("tt1"."tag_id" <> "tt2"."tag_id"))))
     JOIN "content"."tags" "t1" ON ((("t1"."id" = "tt1"."tag_id") AND ("t1"."visibility" = 'public'::"content"."tag_visibility_enum"))))
     JOIN "content"."tags" "t2" ON ((("t2"."id" = "tt2"."tag_id") AND ("t2"."visibility" = 'public'::"content"."tag_visibility_enum"))));


ALTER VIEW "content"."vw_tag_cross_lang" OWNER TO "postgres";


CREATE OR REPLACE VIEW "content"."vw_threads_hot_scores" AS
 SELECT "t"."id",
    "et_orig"."language_code" AS "primary_language",
    ("log"(GREATEST((1)::numeric, (((2.0 * (COALESCE("r"."like_count", (0)::bigint))::numeric) + (3.0 * (COALESCE("t"."reply_count", 0))::numeric)) + (0.5 * (COALESCE("t"."view_count", 0))::numeric)))) / "pow"(((EXTRACT(epoch FROM ("now"() - "t"."created_at")) / 3600.0) + (2)::numeric), 1.5)) AS "hot_score"
   FROM (("content"."threads" "t"
     LEFT JOIN "content"."entity_translations" "et_orig" ON ((("et_orig"."entity_id" = "t"."id") AND ("et_orig"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("et_orig"."is_original" = true))))
     LEFT JOIN ( SELECT "reactions"."entity_id" AS "thread_id",
            "count"(*) FILTER (WHERE ("reactions"."reaction" = 'like'::"content"."reaction_enum")) AS "like_count"
           FROM "content"."reactions"
          WHERE ("reactions"."entity_type" = 'thread'::"content"."entity_type_enum")
          GROUP BY "reactions"."entity_id") "r" ON (("r"."thread_id" = "t"."id")))
  WHERE (("t"."visibility" = 'public'::"content"."visibility_enum") AND ("t"."status" = 'published'::"content"."content_status"));


ALTER VIEW "content"."vw_threads_hot_scores" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "core"."cities" (
    "id" bigint NOT NULL,
    "name" character varying(255) NOT NULL,
    "state_id" bigint NOT NULL,
    "state_code" character varying(255) NOT NULL,
    "country_id" bigint NOT NULL,
    "country_code" character(2) NOT NULL,
    "type" character varying(191),
    "level" integer,
    "parent_id" bigint,
    "latitude" numeric(10,8) NOT NULL,
    "longitude" numeric(11,8) NOT NULL,
    "native" character varying(255),
    "population" bigint,
    "timezone" character varying(255),
    "translations" "text",
    "created_at" timestamp(6) without time zone DEFAULT '2014-01-01 12:01:01'::timestamp without time zone,
    "updated_at" timestamp(6) without time zone DEFAULT "now"(),
    "flag" smallint DEFAULT 1 NOT NULL,
    "wikiDataId" character varying(255)
);


ALTER TABLE "core"."cities" OWNER TO "postgres";


COMMENT ON TABLE "core"."cities" IS 'City reference with geo coordinates and timezone. Belongs to a state and country. ~150K rows seeded from world-cities dataset.';



CREATE SEQUENCE IF NOT EXISTS "core"."cities_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "core"."cities_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "core"."cities_id_seq" OWNED BY "core"."cities"."id";



CREATE TABLE IF NOT EXISTS "core"."countries" (
    "id" bigint NOT NULL,
    "name" character varying(100) NOT NULL,
    "iso3" character(3),
    "numeric_code" character(3),
    "iso2" character(2) NOT NULL,
    "phonecode" character varying(255),
    "capital" character varying(255),
    "currency" character varying(255),
    "currency_name" character varying(255),
    "currency_symbol" character varying(255),
    "tld" character varying(255),
    "native" character varying(255),
    "population" bigint,
    "gdp" bigint,
    "region" character varying(255),
    "region_id" bigint,
    "subregion" character varying(255),
    "subregion_id" bigint,
    "nationality" character varying(255),
    "area_sq_km" double precision,
    "postal_code_format" character varying(255),
    "postal_code_regex" character varying(255),
    "timezones" "text",
    "translations" "text",
    "latitude" numeric(10,8),
    "longitude" numeric(11,8),
    "emoji" character varying(191),
    "emojiU" character varying(191),
    "created_at" timestamp(6) without time zone,
    "updated_at" timestamp(6) without time zone DEFAULT "now"(),
    "flag" smallint DEFAULT 1 NOT NULL,
    "wikiDataId" character varying(255)
);


ALTER TABLE "core"."countries" OWNER TO "postgres";


COMMENT ON TABLE "core"."countries" IS 'Country reference with ISO codes, phone codes, currency info, geo coordinates, emoji flags, timezones, and translations. Seeded from world-cities dataset.';



CREATE SEQUENCE IF NOT EXISTS "core"."countries_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "core"."countries_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "core"."countries_id_seq" OWNED BY "core"."countries"."id";



CREATE TABLE IF NOT EXISTS "core"."currencies" (
    "code" character(3) NOT NULL,
    "name" "text" NOT NULL
);


ALTER TABLE "core"."currencies" OWNER TO "postgres";


COMMENT ON TABLE "core"."currencies" IS 'ISO 4217 currency codes and display names. Read-only reference table.';



CREATE TABLE IF NOT EXISTS "core"."languages" (
    "code" "text" NOT NULL,
    "name" "text" NOT NULL,
    "native_name" "text" NOT NULL,
    "is_default" boolean DEFAULT false,
    "is_active" boolean DEFAULT true,
    "direction" "text" DEFAULT 'ltr'::"text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "languages_code_check" CHECK (("code" ~ '^[a-z]{2}(-[A-Z]{2})?$'::"text")),
    CONSTRAINT "languages_direction_check" CHECK (("direction" = ANY (ARRAY['ltr'::"text", 'rtl'::"text"])))
);


ALTER TABLE "core"."languages" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "core"."regions" (
    "id" bigint NOT NULL,
    "name" character varying(100) NOT NULL,
    "translations" "text",
    "created_at" timestamp(6) without time zone,
    "updated_at" timestamp(6) without time zone DEFAULT "now"(),
    "flag" smallint DEFAULT 1 NOT NULL,
    "wikiDataId" character varying(255)
);


ALTER TABLE "core"."regions" OWNER TO "postgres";


COMMENT ON TABLE "core"."regions" IS 'Geographic region reference (e.g. Africa, Americas, Asia, Europe, Oceania). Seeded from world-cities dataset. Read-only for clients.';



CREATE SEQUENCE IF NOT EXISTS "core"."regions_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "core"."regions_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "core"."regions_id_seq" OWNED BY "core"."regions"."id";



CREATE TABLE IF NOT EXISTS "core"."states" (
    "id" bigint NOT NULL,
    "name" character varying(255) NOT NULL,
    "country_id" bigint NOT NULL,
    "country_code" character(2) NOT NULL,
    "fips_code" character varying(255),
    "iso2" character varying(255),
    "iso3166_2" character varying(10),
    "type" character varying(191),
    "level" integer,
    "parent_id" bigint,
    "native" character varying(255),
    "latitude" numeric(10,8),
    "longitude" numeric(11,8),
    "timezone" character varying(255),
    "translations" "text",
    "created_at" timestamp(6) without time zone,
    "updated_at" timestamp(6) without time zone DEFAULT "now"(),
    "flag" smallint DEFAULT 1 NOT NULL,
    "wikiDataId" character varying(255),
    "population" character varying(255)
);


ALTER TABLE "core"."states" OWNER TO "postgres";


COMMENT ON TABLE "core"."states" IS 'State/province/territory reference. Belongs to a country. Includes ISO 3166-2 codes and geo coordinates.';



CREATE SEQUENCE IF NOT EXISTS "core"."states_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "core"."states_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "core"."states_id_seq" OWNED BY "core"."states"."id";



CREATE TABLE IF NOT EXISTS "core"."subregions" (
    "id" bigint NOT NULL,
    "name" character varying(100) NOT NULL,
    "translations" "text",
    "region_id" bigint NOT NULL,
    "created_at" timestamp(6) without time zone,
    "updated_at" timestamp(6) without time zone DEFAULT "now"(),
    "flag" smallint DEFAULT 1 NOT NULL,
    "wikiDataId" character varying(255)
);


ALTER TABLE "core"."subregions" OWNER TO "postgres";


COMMENT ON TABLE "core"."subregions" IS 'Geographic subregion reference (e.g. Northern Europe, Southeast Asia). Belongs to a region. Seeded from world-cities dataset.';



CREATE SEQUENCE IF NOT EXISTS "core"."subregions_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "core"."subregions_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "core"."subregions_id_seq" OWNED BY "core"."subregions"."id";



CREATE TABLE IF NOT EXISTS "devices"."device_challenges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "device_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "nonce" "text" NOT NULL,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "signature" "text",
    "signed_iat" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "expires_at" timestamp with time zone DEFAULT ("now"() + '24:00:00'::interval) NOT NULL,
    CONSTRAINT "device_challenges_signature_check" CHECK ((("signature" IS NULL) OR ("char_length"("signature") <= 256))),
    CONSTRAINT "device_challenges_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'answered'::"text", 'approved'::"text", 'rejected'::"text", 'expired'::"text"])))
);


ALTER TABLE "devices"."device_challenges" OWNER TO "postgres";


COMMENT ON TABLE "devices"."device_challenges" IS 'One-time challenge nonces issued during fn_device_register_with_key. The pending device signs the nonce with its private key and posts the envelope via fn_device_post_challenge before the owner can approve.';



CREATE TABLE IF NOT EXISTS "devices"."nonce_cache" (
    "nonce" "text" NOT NULL,
    "device_id" "uuid" NOT NULL,
    "expires_at" timestamp with time zone DEFAULT ("now"() + '00:10:00'::interval) NOT NULL
);


ALTER TABLE "devices"."nonce_cache" OWNER TO "postgres";


COMMENT ON TABLE "devices"."nonce_cache" IS 'Replay-protection cache for SignedEnvelope nonces. Retention 10 minutes; matches the iat ±300s window with overlap.';



CREATE TABLE IF NOT EXISTS "devices"."peer_leases" (
    "lease_kind" "text" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "device_id" "uuid" NOT NULL,
    "acquired_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "expires_at" timestamp with time zone NOT NULL,
    CONSTRAINT "peer_leases_lease_kind_check" CHECK ((("char_length"("lease_kind") >= 1) AND ("char_length"("lease_kind") <= 64)))
);


ALTER TABLE "devices"."peer_leases" OWNER TO "postgres";


COMMENT ON TABLE "devices"."peer_leases" IS 'Time-bounded leases for cross-device coordination (e.g. sync_flush). One lease per (lease_kind, lenser_id). Acquired via fn_acquire_leader_lease.';



CREATE TABLE IF NOT EXISTS "devices"."registered_devices" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "device_type" "text" DEFAULT 'other'::"text" NOT NULL,
    "os" "text",
    "arch" "text",
    "cli_version" "text",
    "runner_version" "text",
    "capabilities" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "trust_level" "text" DEFAULT 'pending'::"text" NOT NULL,
    "gateway_status" "text" DEFAULT 'disconnected'::"text" NOT NULL,
    "last_seen_at" timestamp with time zone,
    "revoked_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "public_key" "text",
    "signing_algo" "text",
    "last_heartbeat_at" timestamp with time zone,
    "daemon_version" "text",
    CONSTRAINT "registered_devices_arch_check" CHECK (("char_length"("arch") <= 32)),
    CONSTRAINT "registered_devices_cli_version_check" CHECK (("char_length"("cli_version") <= 32)),
    CONSTRAINT "registered_devices_daemon_version_check" CHECK ((("daemon_version" IS NULL) OR ("char_length"("daemon_version") <= 32))),
    CONSTRAINT "registered_devices_device_type_check" CHECK (("device_type" = ANY (ARRAY['desktop'::"text", 'laptop'::"text", 'server'::"text", 'cloud'::"text", 'other'::"text"]))),
    CONSTRAINT "registered_devices_gateway_status_check" CHECK (("char_length"("gateway_status") <= 32)),
    CONSTRAINT "registered_devices_name_check" CHECK ((("char_length"("name") >= 1) AND ("char_length"("name") <= 100))),
    CONSTRAINT "registered_devices_os_check" CHECK (("char_length"("os") <= 64)),
    CONSTRAINT "registered_devices_public_key_check" CHECK ((("public_key" IS NULL) OR (("char_length"("public_key") >= 32) AND ("char_length"("public_key") <= 256)))),
    CONSTRAINT "registered_devices_runner_version_check" CHECK (("char_length"("runner_version") <= 32)),
    CONSTRAINT "registered_devices_signing_algo_check" CHECK ((("signing_algo" IS NULL) OR ("signing_algo" = 'ed25519'::"text"))),
    CONSTRAINT "registered_devices_trust_level_check" CHECK (("trust_level" = ANY (ARRAY['pending'::"text", 'approved'::"text", 'trusted'::"text", 'offline'::"text", 'revoked'::"text", 'blocked'::"text", 'unhealthy'::"text"]))),
    CONSTRAINT "revoked_at_requires_revoked_state" CHECK ((("revoked_at" IS NULL) OR ("trust_level" = ANY (ARRAY['revoked'::"text", 'blocked'::"text"]))))
);


ALTER TABLE "devices"."registered_devices" OWNER TO "postgres";


COMMENT ON TABLE "devices"."registered_devices" IS 'Persistent registry of developer-owned local machines approved for trusted execution. Created and mutated only via SECURITY DEFINER RPCs — no direct INSERT/UPDATE from clients.';



COMMENT ON COLUMN "devices"."registered_devices"."public_key" IS 'Base64-encoded raw Ed25519 public key (32 bytes). Recorded during the two-step approve flow (fn_device_register_with_key + fn_device_post_challenge).';



COMMENT ON COLUMN "devices"."registered_devices"."signing_algo" IS 'Signature algorithm for envelopes; v1 is always ed25519.';



COMMENT ON COLUMN "devices"."registered_devices"."last_heartbeat_at" IS 'Updated by fn_device_heartbeat; used to derive offline / unhealthy state.';



CREATE TABLE IF NOT EXISTS "devices"."sync_outbox" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "device_id" "uuid" NOT NULL,
    "object_class" "text" NOT NULL,
    "object_id" "text" NOT NULL,
    "op" "text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "vclock" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "applied_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "sync_outbox_object_class_check" CHECK ((("char_length"("object_class") >= 1) AND ("char_length"("object_class") <= 64))),
    CONSTRAINT "sync_outbox_object_id_check" CHECK ((("char_length"("object_id") >= 1) AND ("char_length"("object_id") <= 128))),
    CONSTRAINT "sync_outbox_op_check" CHECK (("op" = ANY (ARRAY['upsert'::"text", 'delete'::"text"])))
);


ALTER TABLE "devices"."sync_outbox" OWNER TO "postgres";


COMMENT ON TABLE "devices"."sync_outbox" IS 'Append-only log of pending local→cloud changes per device. Written by devices.fn_sync_push after envelope verification. 30-day retention; records are archived to audit.events after applied_at + 30 days.';



CREATE TABLE IF NOT EXISTS "devices"."sync_watermarks" (
    "lenser_id" "uuid" NOT NULL,
    "device_id" "uuid" NOT NULL,
    "object_class" "text" NOT NULL,
    "watermark" timestamp with time zone DEFAULT '-infinity'::timestamp with time zone NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "sync_watermarks_object_class_check" CHECK ((("char_length"("object_class") >= 1) AND ("char_length"("object_class") <= 64)))
);


ALTER TABLE "devices"."sync_watermarks" OWNER TO "postgres";


COMMENT ON TABLE "devices"."sync_watermarks" IS 'Per-(device, object_class) cursor into the cloud changefeed for pull. Updated by devices.fn_sync_pull after a successful row return. Idempotent replay returns the same set.';



CREATE TABLE IF NOT EXISTS "execution"."artifact_medias" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "artifact_id" "uuid" NOT NULL,
    "media_id" "uuid" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "execution"."artifact_medias" OWNER TO "postgres";


COMMENT ON TABLE "execution"."artifact_medias" IS 'Links execution artifacts to one or more media objects (images, audio, video). Replaces the deprecated 1:1 media_object_id column on execution.artifacts. Append-only; cascade-deletes when artifact is removed.';



CREATE TABLE IF NOT EXISTS "execution"."artifacts" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "artifact_kind" "text" NOT NULL,
    "content_text" "text",
    "content_json" "jsonb",
    "visibility" "text" DEFAULT 'private'::"text" NOT NULL,
    "is_primary_output" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "output_type" "text",
    CONSTRAINT "artifacts_kind_check" CHECK (("artifact_kind" = ANY (ARRAY['text'::"text", 'image'::"text", 'audio'::"text", 'video'::"text", 'json'::"text", 'trace'::"text", 'tool_log'::"text", 'rubric_result'::"text"]))),
    CONSTRAINT "artifacts_visibility_check" CHECK (("visibility" = ANY (ARRAY['private'::"text", 'public'::"text", 'contender_only'::"text"])))
);


ALTER TABLE "execution"."artifacts" OWNER TO "postgres";


COMMENT ON COLUMN "execution"."artifacts"."output_type" IS 'Extensible output classification. Superset of artifact_kind CHECK constraint. Prefer this column for new writes; artifact_kind retained for legacy compatibility.';



CREATE TABLE IF NOT EXISTS "execution"."attestation_verifications" (
    "attestation_id" "uuid" NOT NULL,
    "verified" boolean DEFAULT false NOT NULL,
    "verified_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "invalid_reason" "text",
    "device_id" "uuid",
    "envelope_kid" "uuid",
    "envelope_iat" timestamp with time zone,
    "envelope_nonce" "text",
    "envelope_hash" "text",
    CONSTRAINT "attestation_verifications_envelope_hash_check" CHECK ((("envelope_hash" IS NULL) OR ("char_length"("envelope_hash") <= 128))),
    CONSTRAINT "attestation_verifications_envelope_nonce_check" CHECK ((("envelope_nonce" IS NULL) OR ("char_length"("envelope_nonce") <= 64))),
    CONSTRAINT "attestation_verifications_invalid_reason_check" CHECK ((("invalid_reason" IS NULL) OR ("char_length"("invalid_reason") <= 256)))
);


ALTER TABLE "execution"."attestation_verifications" OWNER TO "postgres";


COMMENT ON TABLE "execution"."attestation_verifications" IS 'Per-attestation cryptographic verification result. Append-only. Trust evaluation refuses to award execution_verified or higher when no row exists or `verified = false`.';



CREATE TABLE IF NOT EXISTS "execution"."attestations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "device_id" "uuid",
    "signed" boolean DEFAULT false NOT NULL,
    "signature" "text",
    "gateway_verified" boolean DEFAULT false NOT NULL,
    "device_trusted" boolean DEFAULT false NOT NULL,
    "policy_passed" boolean DEFAULT false NOT NULL,
    "workflow_hash" "text",
    "lens_hash" "text",
    "agent_config_hash" "text",
    "runner_version" "text",
    "cli_version" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "envelope_kid" "uuid",
    "envelope_iat" timestamp with time zone,
    "envelope_nonce" "text",
    "envelope_body_jcs_sha256" "text",
    CONSTRAINT "attestations_agent_config_hash_check" CHECK (("char_length"("agent_config_hash") <= 128)),
    CONSTRAINT "attestations_cli_version_check" CHECK (("char_length"("cli_version") <= 32)),
    CONSTRAINT "attestations_lens_hash_check" CHECK (("char_length"("lens_hash") <= 128)),
    CONSTRAINT "attestations_runner_version_check" CHECK (("char_length"("runner_version") <= 32)),
    CONSTRAINT "attestations_signature_check" CHECK (("char_length"("signature") <= 512)),
    CONSTRAINT "attestations_workflow_hash_check" CHECK (("char_length"("workflow_hash") <= 128))
);


ALTER TABLE "execution"."attestations" OWNER TO "postgres";


COMMENT ON TABLE "execution"."attestations" IS 'Execution-level signing metadata captured by the local runner at submission time. Distinct from audit.attestations (platform lifecycle). Append-only — no UPDATE trigger guards it.';



CREATE TABLE IF NOT EXISTS "execution"."execution_tags" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "tag" "text" NOT NULL,
    "severity" "text" DEFAULT 'info'::"text" NOT NULL,
    "metadata" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "execution_tags_severity_check" CHECK (("severity" = ANY (ARRAY['info'::"text", 'warn'::"text", 'critical'::"text"])))
);


ALTER TABLE "execution"."execution_tags" OWNER TO "postgres";


COMMENT ON TABLE "execution"."execution_tags" IS 'Classification tags applied to execution runs by the API Worker. Tags: high_token_usage, repeated_failure, suspicious_prompt, policy_violation, rate_limited. Used for downstream monitoring, throttling, and anomaly detection.';



COMMENT ON COLUMN "execution"."execution_tags"."tag" IS 'Tag identifier. Known values: high_token_usage, repeated_failure, suspicious_prompt, policy_violation, rate_limited.';



CREATE TABLE IF NOT EXISTS "execution"."links" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "entity_type" "text" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "links_entity_type_check" CHECK (("entity_type" = ANY (ARRAY['submission'::"text", 'workflow_run'::"text", 'team_run'::"text", 'attestation'::"text"])))
);


ALTER TABLE "execution"."links" OWNER TO "postgres";


COMMENT ON TABLE "execution"."links" IS 'Link table joining execution.runs to consumer entities (battle submissions, workflow runs, team runs, attestations). Written by SECURITY DEFINER RPCs such as fn_battles_submit. ON CONFLICT DO NOTHING semantics rely on the UNIQUE (run_id, entity_type, entity_id) constraint.';



CREATE TABLE IF NOT EXISTS "execution"."origin_types" (
    "key" "text" NOT NULL,
    "description" "text",
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "execution"."origin_types" OWNER TO "postgres";


COMMENT ON TABLE "execution"."origin_types" IS 'Registry of valid execution request origin types. Adding a new origin is an INSERT here — no DDL required. Replaces hard-coded CHECK constraint on execution.requests.origin_type.';



CREATE TABLE IF NOT EXISTS "execution"."parameter_usage_logs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "request_id" "uuid" NOT NULL,
    "version_id" "uuid",
    "version_parameter_id" "uuid",
    "lenser_id" "uuid" NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "ai_model_id" "uuid",
    "parameter_key" "text" NOT NULL,
    "parameter_type" "text" NOT NULL,
    "value_snapshot" "text",
    "was_required" boolean DEFAULT false NOT NULL,
    "was_empty" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "metadata" "jsonb"
);


ALTER TABLE "execution"."parameter_usage_logs" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "execution"."request_attachments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "request_id" "uuid" NOT NULL,
    "media_object_id" "uuid" NOT NULL,
    "binding_key" "text" NOT NULL,
    "attached_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "execution"."request_attachments" OWNER TO "postgres";


COMMENT ON TABLE "execution"."request_attachments" IS 'GRASP Creator: links execution requests to media.objects for file-type parameters. One row per named binding_key (matches version_parameters.key for file params). Read by the cloud worker to build multimodal API calls. Cascade-deletes with request; media.objects are NEVER deleted via this FK.';



COMMENT ON COLUMN "execution"."request_attachments"."binding_key" IS 'Matches lenses.version_parameters.key for the ''file'' type param. Used by the cloud worker to inject the file into the correct slot in the model prompt.';



CREATE TABLE IF NOT EXISTS "execution"."requests" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "requester_lenser_id" "uuid" NOT NULL,
    "origin_type" "text" NOT NULL,
    "origin_id" "uuid",
    "model_id" "uuid",
    "lens_id" "uuid",
    "input_snapshot" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "runtime_origin" "text" DEFAULT 'cloud'::"text" NOT NULL,
    "funding_source" "text" DEFAULT 'platform_credit'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "byok_key_ref_id" "uuid",
    "is_active" boolean DEFAULT true NOT NULL,
    "version_id" "uuid",
    "workspace_id" "uuid",
    "idempotency_key" "text",
    "output_modality" "text",
    "resolved_artifact_snapshot" "jsonb",
    "resolved_artifact_snapshot_hash" "text",
    "lens_content_hash" "text",
    CONSTRAINT "requests_funding_source_check" CHECK (("funding_source" = ANY (ARRAY['user_byok_cloud'::"text", 'user_byok_local'::"text", 'platform_credit'::"text", 'sponsored'::"text"]))),
    CONSTRAINT "requests_output_modality_check" CHECK (("output_modality" = ANY (ARRAY['text'::"text", 'image'::"text", 'video'::"text", 'audio'::"text", 'music'::"text"]))),
    CONSTRAINT "requests_runtime_origin_check" CHECK (("runtime_origin" = ANY (ARRAY['cloud'::"text", 'local'::"text", 'hybrid'::"text", 'offline_import'::"text"])))
);


ALTER TABLE "execution"."requests" OWNER TO "postgres";


COMMENT ON COLUMN "execution"."requests"."byok_key_ref_id" IS 'FK to agents.byok_key_refs when funding_source=user_byok_cloud. NULL for platform_credit, user_byok_local (key never reaches cloud DB), or sponsored.';



COMMENT ON COLUMN "execution"."requests"."is_active" IS 'Soft-delete flag. Set to false instead of hard-deleting.';



COMMENT ON COLUMN "execution"."requests"."version_id" IS 'The specific lens version this request was made against. Moved from execution.ray_runs (2026-03-25). NULL for requests created before versioning or without a lens context.';



COMMENT ON COLUMN "execution"."requests"."workspace_id" IS 'Tenant boundary for this execution request. Resolved at request time from the lenser''s active workspace (personal workspace for solo users; org workspace when executing on behalf of an organization). NULL for legacy requests pre-workspace-scoping.';



COMMENT ON COLUMN "execution"."requests"."output_modality" IS 'Output media type requested. NULL means text (backward compat). Set for generative media executions.';



COMMENT ON COLUMN "execution"."requests"."resolved_artifact_snapshot" IS 'Redacted execution-time artifact binding. For lenses, stores lens_id/version_id/content_hash only.';



CREATE TABLE IF NOT EXISTS "execution"."runner_device_bindings" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "runner_id" "uuid" NOT NULL,
    "device_id" "uuid" NOT NULL,
    "bound_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    CONSTRAINT "runner_device_bindings_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'paused'::"text", 'revoked'::"text"])))
);


ALTER TABLE "execution"."runner_device_bindings" OWNER TO "postgres";


COMMENT ON TABLE "execution"."runner_device_bindings" IS 'Links a registered runner to a physical trusted device. A runner may be bound to at most one active device at a time. Written only via fn_runner_bind_device.';



CREATE TABLE IF NOT EXISTS "execution"."runners" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "adapter_type" "text" NOT NULL,
    "config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "last_probed_at" timestamp with time zone,
    "last_probe_status" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "runners_adapter_type_check" CHECK (("adapter_type" = ANY (ARRAY['openai-agents'::"text", 'langchain'::"text", 'crewai'::"text", 'mcp'::"text", 'ollama'::"text", 'http'::"text", 'custom'::"text"]))),
    CONSTRAINT "runners_last_probe_status_check" CHECK (("char_length"("last_probe_status") <= 32)),
    CONSTRAINT "runners_name_check" CHECK ((("char_length"("name") >= 1) AND ("char_length"("name") <= 100)))
);


ALTER TABLE "execution"."runners" OWNER TO "postgres";


COMMENT ON TABLE "execution"."runners" IS 'Workspace runners (adapters) registered by a Lenser. Bound to one or more physical devices via execution.runner_device_bindings. Mutated only via SECURITY DEFINER RPCs.';



CREATE TABLE IF NOT EXISTS "execution"."steps" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "ordinal" integer NOT NULL,
    "step_type" "text" NOT NULL,
    "tool_name" "text",
    "input_snapshot" "jsonb",
    "output_snapshot" "jsonb",
    "input_hash" "text",
    "output_hash" "text",
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "latency_ms" integer,
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "step_definition_id" "uuid",
    CONSTRAINT "steps_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'running'::"text", 'succeeded'::"text", 'failed'::"text"]))),
    CONSTRAINT "steps_step_type_check" CHECK (("step_type" = ANY (ARRAY['lens'::"text", 'tool_call'::"text", 'tool_result'::"text", 'model_call'::"text", 'judge_call'::"text", 'retrieval'::"text", 'transform'::"text"])))
);


ALTER TABLE "execution"."steps" OWNER TO "postgres";


COMMENT ON COLUMN "execution"."steps"."step_definition_id" IS 'FK to the lenses.steps definition that spawned this runtime step. NULL for historical runs and ad-hoc steps with no definition. Enables reproducibility diff: what the lens defined vs. what the run executed.';



CREATE TABLE IF NOT EXISTS "execution"."stream_sessions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "provider" "text" NOT NULL,
    "model_key" "text" NOT NULL,
    "status" "text" DEFAULT 'open'::"text" NOT NULL,
    "bytes_sent" integer DEFAULT 0 NOT NULL,
    "token_input" integer,
    "token_output" integer,
    "credit_cost" bigint,
    "error_code" "text",
    "started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "completed_at" timestamp with time zone,
    "workspace_id" "uuid",
    CONSTRAINT "stream_sessions_status_check" CHECK (("status" = ANY (ARRAY['open'::"text", 'completed'::"text", 'failed'::"text", 'timed_out'::"text"])))
);


ALTER TABLE "execution"."stream_sessions" OWNER TO "postgres";


COMMENT ON TABLE "execution"."stream_sessions" IS 'One row per /execute/stream request. Tracks SSE lifecycle for observability.';



COMMENT ON COLUMN "execution"."stream_sessions"."run_id" IS 'References execution.runs.id — ties stream session to the billing run.';



COMMENT ON COLUMN "execution"."stream_sessions"."lenser_id" IS 'Denormalized from execution.requests.requester_lenser_id for fast per-user queries.';



COMMENT ON COLUMN "execution"."stream_sessions"."bytes_sent" IS 'Total SSE bytes sent to client before stream closed.';



COMMENT ON COLUMN "execution"."stream_sessions"."workspace_id" IS 'Denormalized workspace_id from execution.requests (via runs). Stored directly for fast per-workspace stream observability queries without joining through runs → requests.';



CREATE TABLE IF NOT EXISTS "execution"."trust_evaluations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "submission_id" "uuid" NOT NULL,
    "attestation_id" "uuid",
    "trust_level" "text" DEFAULT 'unverified'::"text" NOT NULL,
    "factors" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "evaluated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "trust_evaluations_trust_level_check" CHECK (("trust_level" = ANY (ARRAY['unverified'::"text", 'account_verified'::"text", 'agent_verified'::"text", 'device_verified'::"text", 'runner_verified'::"text", 'execution_verified'::"text", 'fully_trusted'::"text"])))
);


ALTER TABLE "execution"."trust_evaluations" OWNER TO "postgres";


COMMENT ON TABLE "execution"."trust_evaluations" IS 'Computed trust level per battle submission. Updated whenever attestation data changes. One row per submission (UNIQUE constraint).';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_node_results" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "node_id" "uuid" NOT NULL,
    "execution_run_id" "uuid",
    "status" "text" DEFAULT 'pending'::"text" NOT NULL,
    "output_data" "jsonb",
    "error_message" "text",
    "started_at" timestamp with time zone,
    "completed_at" timestamp with time zone,
    "input_tokens" integer DEFAULT 0 NOT NULL,
    "output_tokens" integer DEFAULT 0 NOT NULL,
    "cost_credits" integer DEFAULT 0 NOT NULL,
    "retry_count" integer DEFAULT 0 NOT NULL,
    "duration_ms" integer,
    "ttfb_ms" integer,
    "waiting_reason" "text",
    "resolved_input_snapshot" "jsonb",
    "provider_route" "jsonb",
    "workflow_version_node_id" "uuid",
    "resolved_lens_version_id" "uuid",
    "node_snapshot_hash" "text",
    CONSTRAINT "ck_lenses_workflow_node_results_retry_count_nonneg" CHECK ((("retry_count" IS NULL) OR ("retry_count" >= 0))),
    CONSTRAINT "wnr_cost_credits_nonneg" CHECK (("cost_credits" >= 0)),
    CONSTRAINT "wnr_duration_ms_nonneg" CHECK ((("duration_ms" IS NULL) OR ("duration_ms" >= 0))),
    CONSTRAINT "wnr_input_tokens_nonneg" CHECK (("input_tokens" >= 0)),
    CONSTRAINT "wnr_output_tokens_nonneg" CHECK (("output_tokens" >= 0)),
    CONSTRAINT "wnr_retry_count_nonneg" CHECK (("retry_count" >= 0)),
    CONSTRAINT "wnr_ttfb_ms_nonneg" CHECK ((("ttfb_ms" IS NULL) OR ("ttfb_ms" >= 0))),
    CONSTRAINT "workflow_node_results_status_check" CHECK (("status" = ANY (ARRAY['pending'::"text", 'awaiting_dependency'::"text", 'queued'::"text", 'running'::"text", 'streaming'::"text", 'retrying'::"text", 'completed'::"text", 'failed'::"text", 'cancelled'::"text", 'skipped'::"text", 'timed_out'::"text", 'blocked'::"text", 'invalidated'::"text"])))
);


ALTER TABLE "lenses"."workflow_node_results" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_node_results" IS 'Per-node execution result for a workflow run. Seeded with status=pending by fn_start_workflow_run. The CF Worker updates status and output_data as nodes complete. Realtime subscriptions on this table drive the live progress UI.';



COMMENT ON COLUMN "lenses"."workflow_node_results"."retry_count" IS 'Number of provider retries consumed for this node. 0 on first success; increments each time NodeRuntime applies a retry policy before terminal success/failure.';



COMMENT ON COLUMN "lenses"."workflow_node_results"."duration_ms" IS 'Wall-clock duration of the final (successful or terminal) provider attempt, in milliseconds. NULL when the node is skipped/blocked before any attempt.';



COMMENT ON COLUMN "lenses"."workflow_node_results"."ttfb_ms" IS 'Time-to-first-byte on streamed nodes, in milliseconds. Written from the first `partial` chunk observed by NodeRuntime; NULL for non-streaming lenses.';



COMMENT ON COLUMN "lenses"."workflow_node_results"."waiting_reason" IS 'Why this node is currently waiting. NULL when the node is not waiting. Conventional values: dependency, condition_false, rate_limit, retry_backoff, human_input, external_callback, queued.';



COMMENT ON COLUMN "lenses"."workflow_node_results"."resolved_input_snapshot" IS 'Label→value map after edge merge + root inputs, before prompt rendering (redact secrets at write time in callers).';



COMMENT ON COLUMN "lenses"."workflow_node_results"."provider_route" IS 'JSON metadata for the execution route used for this node (e.g. provider id, model key).';



COMMENT ON CONSTRAINT "workflow_node_results_status_check" ON "lenses"."workflow_node_results" IS 'Aligns with the engine state machine (Phase 1 §5). Terminal statuses: completed, failed, cancelled, skipped, timed_out, blocked, invalidated. Transient: pending, awaiting_dependency, queued, running, streaming, retrying.';



CREATE OR REPLACE VIEW "execution"."vw_workflow_run_timeline" AS
 SELECT "r"."id" AS "run_id",
    "r"."workflow_id",
    'run_started'::"text" AS "event_type",
    NULL::"uuid" AS "node_id",
    NULL::"uuid" AS "workflow_node_result_id",
    COALESCE("r"."started_at", "r"."created_at") AS "occurred_at",
    "r"."status",
    NULL::"jsonb" AS "output_data",
    NULL::"text" AS "error_message"
   FROM "lenses"."workflow_runs" "r"
UNION ALL
 SELECT "r"."id" AS "run_id",
    "r"."workflow_id",
    'run_completed'::"text" AS "event_type",
    NULL::"uuid" AS "node_id",
    NULL::"uuid" AS "workflow_node_result_id",
    "r"."completed_at" AS "occurred_at",
    "r"."status",
    NULL::"jsonb" AS "output_data",
    NULL::"text" AS "error_message"
   FROM "lenses"."workflow_runs" "r"
  WHERE ("r"."completed_at" IS NOT NULL)
UNION ALL
 SELECT "nr"."run_id",
    "r"."workflow_id",
    'node_started'::"text" AS "event_type",
    "nr"."node_id",
    "nr"."id" AS "workflow_node_result_id",
    "nr"."started_at" AS "occurred_at",
    "nr"."status",
    NULL::"jsonb" AS "output_data",
    NULL::"text" AS "error_message"
   FROM ("lenses"."workflow_node_results" "nr"
     JOIN "lenses"."workflow_runs" "r" ON (("r"."id" = "nr"."run_id")))
  WHERE ("nr"."started_at" IS NOT NULL)
UNION ALL
 SELECT "nr"."run_id",
    "r"."workflow_id",
    'node_completed'::"text" AS "event_type",
    "nr"."node_id",
    "nr"."id" AS "workflow_node_result_id",
    "nr"."completed_at" AS "occurred_at",
    "nr"."status",
    "nr"."output_data",
    "nr"."error_message"
   FROM ("lenses"."workflow_node_results" "nr"
     JOIN "lenses"."workflow_runs" "r" ON (("r"."id" = "nr"."run_id")))
  WHERE ("nr"."completed_at" IS NOT NULL);


ALTER VIEW "execution"."vw_workflow_run_timeline" OWNER TO "postgres";


COMMENT ON VIEW "execution"."vw_workflow_run_timeline" IS 'Chronological event stream for a workflow run: run_started, run_completed, node_started, node_completed. Consumed by the observability UI (Phase 6) to render Gantt-style timelines without client-side joins. Respects the underlying RLS on lenses.workflow_runs and lenses.workflow_node_results.';



CREATE TABLE IF NOT EXISTS "identity_gov"."reserved_namespaces" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "entry_kind" "text" NOT NULL,
    "value" "text" NOT NULL,
    "class" "text" NOT NULL,
    "deny_score" smallint DEFAULT 80 NOT NULL,
    "reason" "text",
    "source" "text" DEFAULT 'manifest'::"text" NOT NULL,
    "expires_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "reserved_namespaces_class_check" CHECK (("class" = ANY (ARRAY['system'::"text", 'security'::"text", 'provider'::"text", 'model'::"text", 'future'::"text", 'verified_only'::"text", 'restricted'::"text"]))),
    CONSTRAINT "reserved_namespaces_deny_score_range" CHECK ((("deny_score" >= 0) AND ("deny_score" <= 100))),
    CONSTRAINT "reserved_namespaces_kind_check" CHECK (("entry_kind" = ANY (ARRAY['exact'::"text", 'prefix'::"text", 'suffix'::"text", 'token'::"text", 'regex'::"text"]))),
    CONSTRAINT "reserved_namespaces_source_check" CHECK (("source" = ANY (ARRAY['canonical'::"text", 'manifest'::"text", 'ai_inferred'::"text"])))
);

ALTER TABLE ONLY "identity_gov"."reserved_namespaces" FORCE ROW LEVEL SECURITY;


ALTER TABLE "identity_gov"."reserved_namespaces" OWNER TO "postgres";


COMMENT ON TABLE "identity_gov"."reserved_namespaces" IS 'Registry of protected handles and patterns. entry_kind drives probe selection: exact=direct match, prefix/suffix=anchored substring, token=combinatorial composition guard, regex=arbitrary pattern for complex cases.';



COMMENT ON COLUMN "identity_gov"."reserved_namespaces"."entry_kind" IS 'exact: normalized handle must equal value. prefix: normalized handle must not start with value. suffix: normalized handle must not end with value. token: normalized handle must not contain value adjacent to a modifier (ai, bot, admin…). regex: normalized handle must not match value as a POSIX regex.';



COMMENT ON COLUMN "identity_gov"."reserved_namespaces"."value" IS 'Already-normalized form: lowercase, ASCII-only where possible. Regex entries use POSIX syntax.';



COMMENT ON COLUMN "identity_gov"."reserved_namespaces"."deny_score" IS 'Minimum risk score returned when this entry matches. system=100, security=95, provider=90, model=85, future=80, verified_only=70, restricted=50.';



COMMENT ON COLUMN "identity_gov"."reserved_namespaces"."expires_at" IS 'NULL = permanent. Set to now()+30d for ai_inferred entries during grace window.';



CREATE TABLE IF NOT EXISTS "integrations"."tool_allowlist" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "tool_id" "uuid" NOT NULL,
    "config_override" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "granted_by" "uuid" NOT NULL,
    "granted_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "integrations"."tool_allowlist" OWNER TO "postgres";


COMMENT ON TABLE "integrations"."tool_allowlist" IS 'Per-agent tool permission grants (renamed from agent_tool_allowlist).';



CREATE TABLE IF NOT EXISTS "integrations"."tools" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "key" "text" NOT NULL,
    "display_name" "text" NOT NULL,
    "description" "text",
    "schema_version" "text" DEFAULT '1.0'::"text" NOT NULL,
    "input_schema" "jsonb",
    "output_schema" "jsonb",
    "is_system" boolean DEFAULT false NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "integrations"."tools" OWNER TO "postgres";


COMMENT ON TABLE "integrations"."tools" IS 'Agent tool registry. OSS-safe: schemas only, no credentials.';



COMMENT ON COLUMN "integrations"."tools"."is_system" IS 'true = platform-owned tool; false = community-contributed.';



CREATE TABLE IF NOT EXISTS "lensers"."badges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "type" "lensers"."lenser_badge_type" NOT NULL,
    "category" "lensers"."lenser_badge_category" DEFAULT 'achievement'::"lensers"."lenser_badge_category" NOT NULL,
    "label" "text" NOT NULL,
    "description" "text",
    "icon" "text",
    "awarded_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "xp_event_id" "uuid"
);


ALTER TABLE "lensers"."badges" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lensers"."group_members" (
    "group_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "role" "lensers"."group_member_role_enum" DEFAULT 'member'::"lensers"."group_member_role_enum" NOT NULL,
    "permissions" "jsonb" DEFAULT '{}'::"jsonb",
    "joined_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "lensers"."group_members" OWNER TO "postgres";


COMMENT ON TABLE "lensers"."group_members" IS 'Membership junction between lensers.groups and lensers.profiles. Composite PK (group_id, lenser_id) prevents duplicate membership. CASCADE on both FKs: deleting a group removes all memberships; deleting a profile removes that lenser from all groups.';



COMMENT ON COLUMN "lensers"."group_members"."role" IS 'Member role within the group. admin and moderator have elevated privileges. judge is a special role for battle adjudication contexts.';



COMMENT ON COLUMN "lensers"."group_members"."permissions" IS 'Fine-grained permission overrides as JSON. Empty object by default. Application layer interprets keys; database stores opaquely.';



CREATE TABLE IF NOT EXISTS "lensers"."groups" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "owner_lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "slug" "text" NOT NULL,
    "type" "lensers"."group_type_enum" DEFAULT 'community'::"lensers"."group_type_enum" NOT NULL,
    "visibility" "lensers"."group_visibility_enum" DEFAULT 'public'::"lensers"."group_visibility_enum" NOT NULL,
    "description" "text",
    "avatar_url" "text",
    "max_members" integer DEFAULT 100,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "groups_max_members_check" CHECK (("max_members" > 0))
);


ALTER TABLE "lensers"."groups" OWNER TO "postgres";


COMMENT ON TABLE "lensers"."groups" IS 'Community-side grouping entity. Owned by a lenser profile. Visibility controls discoverability: public groups appear in search; private and invite_only require membership or invitation.';



COMMENT ON COLUMN "lensers"."groups"."owner_lenser_id" IS 'Profile that created and owns the group. ON DELETE RESTRICT prevents orphaning a group; ownership must be transferred before profile deletion.';



COMMENT ON COLUMN "lensers"."groups"."slug" IS 'URL-safe unique identifier for the group. Used in routes like /g/<slug>.';



COMMENT ON COLUMN "lensers"."groups"."max_members" IS 'Soft cap on membership count. Enforced at application layer. Must be > 0.';



CREATE TABLE IF NOT EXISTS "lensers"."preferences" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "language" "text" DEFAULT 'en'::"text" NOT NULL,
    "notifications" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "sidebar" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "content_visibility" "text" DEFAULT 'public'::"text" NOT NULL,
    "email_digest" boolean DEFAULT true NOT NULL,
    "ai_provider_key" "text",
    "ai_model_key" "text",
    "ai_persona" "text",
    "ai_ruleset" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "wallet_mode" "lensers"."wallet_mode_enum" DEFAULT 'CLOUD'::"lensers"."wallet_mode_enum" NOT NULL,
    "ai_data_usage" boolean DEFAULT false NOT NULL,
    "hide_actions" boolean DEFAULT false NOT NULL,
    "cron_config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "selected_api_key_id" "uuid",
    "country_id" "bpchar",
    "currency" "bpchar",
    "theme" "text" DEFAULT 'system'::"text" NOT NULL,
    "active_lenser_id" "uuid",
    "autoplay_music" boolean DEFAULT true NOT NULL,
    CONSTRAINT "preferences_content_visibility_check" CHECK (("content_visibility" = ANY (ARRAY['public'::"text", 'community'::"text", 'private'::"text"]))),
    CONSTRAINT "preferences_theme_check" CHECK (("theme" = ANY (ARRAY['light'::"text", 'dark'::"text", 'system'::"text"])))
);


ALTER TABLE "lensers"."preferences" OWNER TO "postgres";


COMMENT ON TABLE "lensers"."preferences" IS '1:1 preferences row per lenser profile. Replaces ad-hoc preferences JSONB on lensers.profiles. Auto-created by trigger trg_create_default_preferences on profile insert. Table-level grants added in migration 000061 (authenticated: SELECT/INSERT/UPDATE).';



COMMENT ON COLUMN "lensers"."preferences"."language" IS 'ISO 639-1 language code. NOT NULL, defaults to ''en''.';



COMMENT ON COLUMN "lensers"."preferences"."ai_provider_key" IS 'Preferred AI provider key (references ai.providers.key). Nullable.';



COMMENT ON COLUMN "lensers"."preferences"."ai_model_key" IS 'Preferred AI model key (references ai.models.key). Nullable.';



COMMENT ON COLUMN "lensers"."preferences"."wallet_mode" IS 'Billing mode: CLOUD (platform credits), BYOK (bring-your-own-key), LOCAL (self-hosted). Typed as lensers.wallet_mode_enum. Default: CLOUD.';



COMMENT ON COLUMN "lensers"."preferences"."selected_api_key_id" IS 'The API key the Lenser has chosen to use for AI executions. References ai.keys(id). Nullable — NULL means "use platform default". Auto-cleared (SET NULL) when the referenced key is revoked/deleted.';



COMMENT ON COLUMN "lensers"."preferences"."active_lenser_id" IS 'Currently active lenser profile for workspace switching. NULL = human profile. Only valid when the referenced profile is an AI lenser owned by this user.';



COMMENT ON COLUMN "lensers"."preferences"."autoplay_music" IS 'When true, the LenserFight arena soundtrack autoplays on Battle Detail pages.';



CREATE TABLE IF NOT EXISTS "lensers"."relationships" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "source_profile_id" "uuid" NOT NULL,
    "target_profile_id" "uuid" NOT NULL,
    "status" "lensers"."relationship_status" DEFAULT 'pending'::"lensers"."relationship_status" NOT NULL,
    "requested_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "responded_at" timestamp with time zone,
    "accepted_at" timestamp with time zone,
    "removed_at" timestamp with time zone,
    "is_close_circle" boolean DEFAULT false NOT NULL,
    CONSTRAINT "chk_no_self_rel" CHECK (("source_profile_id" <> "target_profile_id"))
);


ALTER TABLE "lensers"."relationships" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lensers"."social_links" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "platform" "lensers"."lenser_social_platform" NOT NULL,
    "url" "text" NOT NULL,
    "label" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "lenser_social_links_url_https_check" CHECK (("url" ~* '^https://'::"text")),
    CONSTRAINT "lenser_social_links_url_platform_check" CHECK (
CASE "platform"
    WHEN 'Behance'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?behance\.net/'::"text")
    WHEN 'Dribbble'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?dribbble\.com/'::"text")
    WHEN 'GitHub'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?github\.com/'::"text")
    WHEN 'Instagram'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?instagram\.com/'::"text")
    WHEN 'LinkedIn'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?linkedin\.com/'::"text")
    WHEN 'Twitch'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?twitch\.tv/'::"text")
    WHEN 'X'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?(x\.com|twitter\.com)/'::"text")
    WHEN 'Twitter'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?(twitter\.com|x\.com)/'::"text")
    WHEN 'Youtube'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://(www\.)?youtube\.com/|^https://youtu\.be/'::"text")
    WHEN 'Website'::"lensers"."lenser_social_platform" THEN ("url" ~* '^https://'::"text")
    ELSE false
END)
);


ALTER TABLE "lensers"."social_links" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lensers"."tag_follows" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "tag_id" "uuid" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "lensers"."tag_follows" OWNER TO "postgres";


CREATE OR REPLACE VIEW "lensers"."vw_lensers_score" AS
 SELECT "lp"."id" AS "lenser_id",
    "lp"."handle",
    "lp"."display_name",
    "lp"."avatar_url",
    COALESCE("xt"."total_xp", (0)::numeric) AS "total_xp",
    COALESCE("xt"."current_level", 1) AS "current_level",
    (((0.7)::double precision * "log"(GREATEST((1.0)::double precision, (COALESCE("xt"."total_xp", (0)::numeric))::double precision))) + ((0.3)::double precision * "log"(GREATEST((1.0)::double precision, (COALESCE("re7"."recent_reactions", (0)::bigint))::double precision)))) AS "lenser_score"
   FROM (("lensers"."profiles" "lp"
     LEFT JOIN ( SELECT "totals"."lenser_id",
            "sum"("totals"."total_xp") AS "total_xp",
            "max"("totals"."current_level") AS "current_level"
           FROM "xp"."totals"
          GROUP BY "totals"."lenser_id") "xt" ON (("xt"."lenser_id" = "lp"."id")))
     LEFT JOIN ( SELECT "all_recent"."lenser_id",
            "count"(*) AS "recent_reactions"
           FROM ( SELECT "t"."lenser_id"
                   FROM ("content"."reactions" "r"
                     JOIN "content"."threads" "t" ON (("t"."id" = "r"."entity_id")))
                  WHERE (("r"."created_at" > ("now"() - '7 days'::interval)) AND ("r"."entity_type" = 'thread'::"content"."entity_type_enum"))
                UNION ALL
                 SELECT "pt"."lenser_id"
                   FROM ("content"."reactions" "r"
                     JOIN "lenses"."lenses" "pt" ON (("pt"."id" = "r"."entity_id")))
                  WHERE (("r"."created_at" > ("now"() - '7 days'::interval)) AND ("r"."entity_type" = 'lens'::"content"."entity_type_enum"))) "all_recent"
          GROUP BY "all_recent"."lenser_id") "re7" ON (("re7"."lenser_id" = "lp"."id")))
  WHERE (("lp"."status" = 'active'::"lensers"."lenser_status") AND ("lp"."visibility" = 'public'::"lensers"."lenser_visibility") AND ("lp"."deletion_requested_at" IS NULL));


ALTER VIEW "lensers"."vw_lensers_score" OWNER TO "postgres";


COMMENT ON VIEW "lensers"."vw_lensers_score" IS 'Lenser scoring view for recommendation and discovery ranking. Combines XP totals with recent reaction activity (7-day window). Updated to use unified content.reactions table (migration 20260440000008).';



CREATE TABLE IF NOT EXISTS "lenses"."capability_index" (
    "content_hash" "bytea" NOT NULL,
    "lens_kind" "text" NOT NULL,
    "capability_tags" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "input_kinds" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "output_kinds" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "summary" "text" NOT NULL,
    "fts_doc" "tsvector" NOT NULL,
    "indexed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "capability_index_lens_kind_check" CHECK (("lens_kind" = ANY (ARRAY['text'::"text", 'image'::"text", 'video'::"text", 'audio'::"text", 'music'::"text", 'research'::"text", 'pdf'::"text", 'transform'::"text", 'orchestration'::"text", 'validation'::"text", 'routing'::"text"]))),
    CONSTRAINT "capability_index_summary_length" CHECK ((("length"(TRIM(BOTH FROM "summary")) >= 1) AND ("length"(TRIM(BOTH FROM "summary")) <= 300)))
);

ALTER TABLE ONLY "lenses"."capability_index" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."capability_index" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."capability_index" IS 'AI-explorable projection of published Lens contracts. fts_doc combines name, summary, capability_tags, parameter labels, output kinds for keyword search. Embedding column deferred until pgvector is available.';



CREATE TABLE IF NOT EXISTS "lenses"."contract_channels" (
    "lens_id" "uuid" NOT NULL,
    "channel" "text" NOT NULL,
    "content_hash" "bytea" NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_by" "uuid",
    CONSTRAINT "contract_channels_channel_check" CHECK (("channel" = ANY (ARRAY['stable'::"text", 'beta'::"text", 'canary'::"text", 'deprecated'::"text", 'yanked'::"text"])))
);

ALTER TABLE ONLY "lenses"."contract_channels" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."contract_channels" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."contract_channels" IS 'Mutable pointer (lens_id, channel) -> content_hash. The only mutable artifact in the registry. Channel promotion enforced by fn_lens_promote_channel RPC (future).';



CREATE TABLE IF NOT EXISTS "lenses"."contract_signatures" (
    "content_hash" "bytea" NOT NULL,
    "key_id" "text" NOT NULL,
    "algorithm" "text" NOT NULL,
    "signature" "bytea" NOT NULL,
    "signed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "contract_signatures_algorithm_check" CHECK (("algorithm" = ANY (ARRAY['ed25519'::"text", 'hmac-sha256'::"text"]))),
    CONSTRAINT "contract_signatures_key_id_format" CHECK (("key_id" ~ '^[a-z0-9][a-z0-9._-]{0,63}$'::"text")),
    CONSTRAINT "contract_signatures_signature_length" CHECK ((("octet_length"("signature") >= 32) AND ("octet_length"("signature") <= 256)))
);

ALTER TABLE ONLY "lenses"."contract_signatures" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."contract_signatures" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."contract_signatures" IS 'Detached signatures over content_hash. Multi-key to support rotation: (content_hash, key_id) primary key. Registry private key signs at publish; verifiers fetch by key_id.';



CREATE TABLE IF NOT EXISTS "lenses"."contracts" (
    "content_hash" "bytea" NOT NULL,
    "lens_id" "uuid" NOT NULL,
    "version_id" "uuid" NOT NULL,
    "semver" "text" NOT NULL,
    "kind" "text" DEFAULT 'lens'::"text" NOT NULL,
    "spec_version" "text" DEFAULT '1.0.0'::"text" NOT NULL,
    "body" "jsonb" NOT NULL,
    "supersedes_hash" "bytea",
    "published_by" "uuid" NOT NULL,
    "published_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "contracts_body_shape_check" CHECK ((("jsonb_typeof"("body") = 'object'::"text") AND ("jsonb_typeof"(("body" -> 'inputs'::"text")) = 'array'::"text") AND ("jsonb_typeof"(("body" -> 'outputs'::"text")) = 'array'::"text"))),
    CONSTRAINT "contracts_content_hash_length_check" CHECK (("octet_length"("content_hash") = 32)),
    CONSTRAINT "contracts_kind_check" CHECK (("kind" = ANY (ARRAY['lens'::"text", 'workflow'::"text", 'composite'::"text"]))),
    CONSTRAINT "contracts_no_self_supersedes" CHECK ((("supersedes_hash" IS NULL) OR ("supersedes_hash" <> "content_hash"))),
    CONSTRAINT "contracts_semver_format_check" CHECK (("semver" ~ '^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9]+(\.[0-9]+)?)?$'::"text")),
    CONSTRAINT "contracts_spec_version_format_check" CHECK (("spec_version" ~ '^[0-9]+\.[0-9]+\.[0-9]+$'::"text"))
);

ALTER TABLE ONLY "lenses"."contracts" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."contracts" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."contracts" IS 'Immutable signed Lens Contracts. Primary key is sha256(canonical_json(body)). Append-only: see trg_contracts_immutable. Authoritative source of truth for parameter governance.';



CREATE TABLE IF NOT EXISTS "lenses"."dependency_edges" (
    "parent_content_hash" "bytea" NOT NULL,
    "child_content_hash" "bytea" NOT NULL,
    "binding" "text" DEFAULT 'lift'::"text" NOT NULL,
    "depth" integer NOT NULL,
    "edge_metadata" "jsonb",
    CONSTRAINT "dependency_edges_binding_check" CHECK (("binding" = ANY (ARRAY['lift'::"text", 'bind'::"text", 'ref'::"text"]))),
    CONSTRAINT "dependency_edges_depth_nonneg" CHECK ((("depth" >= 1) AND ("depth" <= 16))),
    CONSTRAINT "dependency_edges_metadata_shape" CHECK ((("edge_metadata" IS NULL) OR ("jsonb_typeof"("edge_metadata") = 'object'::"text"))),
    CONSTRAINT "dependency_edges_no_self" CHECK (("parent_content_hash" <> "child_content_hash"))
);

ALTER TABLE ONLY "lenses"."dependency_edges" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."dependency_edges" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."dependency_edges" IS 'Materialized DAG of composite Lens / workflow dependencies. Depth precomputed at publish; cycle detection enforced before insert. Max depth 16.';



CREATE TABLE IF NOT EXISTS "lenses"."execution_records" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "content_hash" "bytea" NOT NULL,
    "principal_lenser_id" "uuid",
    "principal_kind" "text" DEFAULT 'lenser'::"text" NOT NULL,
    "compiled_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "execution_status" "text" DEFAULT 'compiled'::"text" NOT NULL,
    "resolved_inputs" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "violation" "jsonb",
    "trace_id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "idempotency_key" "uuid",
    CONSTRAINT "execution_records_principal_kind_check" CHECK (("principal_kind" = ANY (ARRAY['lenser'::"text", 'ai_lenser'::"text", 'system'::"text", 'gateway'::"text"]))),
    CONSTRAINT "execution_records_resolved_inputs_shape" CHECK (("jsonb_typeof"("resolved_inputs") = 'object'::"text")),
    CONSTRAINT "execution_records_status_check" CHECK (("execution_status" = ANY (ARRAY['compiled'::"text", 'dispatched'::"text", 'completed'::"text", 'failed'::"text", 'rejected'::"text"]))),
    CONSTRAINT "execution_records_violation_consistency" CHECK (((("execution_status" = 'rejected'::"text") AND ("violation" IS NOT NULL)) OR ("execution_status" <> 'rejected'::"text"))),
    CONSTRAINT "execution_records_violation_shape" CHECK ((("violation" IS NULL) OR ("jsonb_typeof"("violation") = 'object'::"text")))
);

ALTER TABLE ONLY "lenses"."execution_records" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."execution_records" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."execution_records" IS 'Append-only audit of every Compiler invocation. One row per compile attempt: violations included for rejected, resolved inputs (redacted for secrets) for accepted. Trace id correlates with audit.events hash chain.';



CREATE TABLE IF NOT EXISTS "lenses"."parameter_contracts" (
    "content_hash" "bytea" NOT NULL,
    "label" "text" NOT NULL,
    "tool_id" "uuid",
    "classification" "text" DEFAULT 'public'::"text" NOT NULL,
    "kind" "text" DEFAULT 'primitive'::"text" NOT NULL,
    "type" "text" NOT NULL,
    "required" boolean DEFAULT false NOT NULL,
    "default_spec" "jsonb",
    "validation" "jsonb",
    "scope" "text" DEFAULT 'lens'::"text" NOT NULL,
    "overrideable_by" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "deprecation" "jsonb",
    "sort_order" integer DEFAULT 0 NOT NULL,
    CONSTRAINT "pc_classification_check" CHECK (("classification" = ANY (ARRAY['public'::"text", 'internal'::"text", 'protected'::"text", 'system'::"text"]))),
    CONSTRAINT "pc_default_spec_shape" CHECK ((("default_spec" IS NULL) OR ("jsonb_typeof"("default_spec") = 'object'::"text"))),
    CONSTRAINT "pc_deprecation_shape" CHECK ((("deprecation" IS NULL) OR ("jsonb_typeof"("deprecation") = 'object'::"text"))),
    CONSTRAINT "pc_kind_check" CHECK (("kind" = ANY (ARRAY['primitive'::"text", 'ai'::"text", 'runtime'::"text"]))),
    CONSTRAINT "pc_label_format_check" CHECK (("label" ~ '^[a-z][a-z0-9_]*$'::"text")),
    CONSTRAINT "pc_label_not_blank" CHECK (("length"(TRIM(BOTH FROM "label")) > 0)),
    CONSTRAINT "pc_protected_override_consistency" CHECK ((("classification" <> ALL (ARRAY['protected'::"text", 'system'::"text"])) OR ("overrideable_by" <@ ARRAY['admin'::"text"]))),
    CONSTRAINT "pc_scope_check" CHECK (("scope" = ANY (ARRAY['lens'::"text", 'workflow'::"text", 'run'::"text", 'tenant'::"text", 'global'::"text"]))),
    CONSTRAINT "pc_validation_shape" CHECK ((("validation" IS NULL) OR ("jsonb_typeof"("validation") = 'object'::"text")))
);

ALTER TABLE ONLY "lenses"."parameter_contracts" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."parameter_contracts" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."parameter_contracts" IS 'Per-contract frozen parameter rules. PK (content_hash, label). Append-only: parameter contracts mutate by publishing a new contract.';



CREATE TABLE IF NOT EXISTS "lenses"."parameter_deprecations" (
    "from_content_hash" "bytea" NOT NULL,
    "label" "text" NOT NULL,
    "to_content_hash" "bytea",
    "to_label" "text",
    "migration_recipe" "jsonb",
    "deprecated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "removal_planned_at" timestamp with time zone,
    CONSTRAINT "parameter_deprecations_label_format" CHECK (("label" ~ '^[a-z][a-z0-9_]*$'::"text")),
    CONSTRAINT "parameter_deprecations_recipe_shape" CHECK ((("migration_recipe" IS NULL) OR ("jsonb_typeof"("migration_recipe") = 'object'::"text"))),
    CONSTRAINT "parameter_deprecations_removal_after_deprecated" CHECK ((("removal_planned_at" IS NULL) OR ("removal_planned_at" >= "deprecated_at"))),
    CONSTRAINT "parameter_deprecations_to_label_format" CHECK ((("to_label" IS NULL) OR ("to_label" ~ '^[a-z][a-z0-9_]*$'::"text")))
);

ALTER TABLE ONLY "lenses"."parameter_deprecations" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."parameter_deprecations" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."schedule_calendars" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "name" "text" NOT NULL,
    "kind" "text" NOT NULL,
    "dates" "date"[] DEFAULT '{}'::"date"[] NOT NULL,
    "timezone" "text" NOT NULL,
    "is_seed" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "schedule_calendars_kind_check" CHECK (("kind" = ANY (ARRAY['skip_dates'::"text", 'only_dates'::"text"])))
);


ALTER TABLE "lenses"."schedule_calendars" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."schedule_calendars" IS 'Phase W1: per-lenser calendar overlays (skip_dates / only_dates) attached to workflow schedules. Seeds (is_seed=true) are owned by the all-zeros UUID sentinel and visible to all authenticated users; only the owner may mutate a non-seed calendar, and seeds are immutable through the API.';



COMMENT ON COLUMN "lenses"."schedule_calendars"."lenser_id" IS 'Owning lenser. For seed rows this is the all-zeros UUID sentinel (00000000-0000-0000-0000-000000000000); see is_seed flag.';



COMMENT ON COLUMN "lenses"."schedule_calendars"."timezone" IS 'IANA timezone name (e.g. America/New_York). Used to resolve ''now'' to a calendar-local date when evaluating skip/only membership.';



CREATE TABLE IF NOT EXISTS "lenses"."security_scopes" (
    "content_hash" "bytea" NOT NULL,
    "required_scope" "text" NOT NULL,
    "scope_kind" "text" DEFAULT 'rbac'::"text" NOT NULL,
    "abac_predicate" "jsonb",
    CONSTRAINT "security_scopes_abac_consistency" CHECK (((("scope_kind" = 'rbac'::"text") AND ("abac_predicate" IS NULL)) OR (("scope_kind" = 'abac'::"text") AND ("jsonb_typeof"("abac_predicate") = 'object'::"text")))),
    CONSTRAINT "security_scopes_kind_check" CHECK (("scope_kind" = ANY (ARRAY['rbac'::"text", 'abac'::"text"]))),
    CONSTRAINT "security_scopes_scope_format" CHECK (("required_scope" ~ '^[a-z][a-z0-9_]*(:[a-z][a-z0-9_]*)+$'::"text"))
);

ALTER TABLE ONLY "lenses"."security_scopes" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."security_scopes" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."steps" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lens_id" "uuid" NOT NULL,
    "version_id" "uuid",
    "ordinal" integer NOT NULL,
    "step_type" "text" NOT NULL,
    "instruction" "text",
    "model_id" "uuid",
    "input_map" "jsonb",
    "output_key" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "sub_lens_id" "uuid",
    CONSTRAINT "lens_steps_step_type_check" CHECK (("step_type" = ANY (ARRAY['lens'::"text", 'tool_call'::"text", 'model_call'::"text", 'retrieval'::"text", 'transform'::"text"])))
);


ALTER TABLE "lenses"."steps" OWNER TO "postgres";


COMMENT ON COLUMN "lenses"."steps"."lens_id" IS 'DEPRECATED: derivable via version_id → lenses.versions.lens_id. Retained for legacy reads and foreign key constraints. Do not write new code that reads this column directly — join via version_id instead. Scheduled for DROP after 2026-Q2.';



COMMENT ON COLUMN "lenses"."steps"."version_id" IS 'Target FK for this step. Once all legacy rows are backfilled (lens_id should be derivable from here), lenses.steps.lens_id will be dropped.';



COMMENT ON COLUMN "lenses"."steps"."sub_lens_id" IS 'FK to the lens this step delegates to when step_type = ''lens''. Extracted from input_map->>''"$lens"'' for FK enforcement. Must be non-NULL when step_type = ''lens''.';



CREATE TABLE IF NOT EXISTS "lenses"."tools" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "key" "text" NOT NULL,
    "label" "text",
    "description" "text",
    "category" "text" DEFAULT 'input'::"text" NOT NULL,
    "type" "text" NOT NULL,
    "required" boolean DEFAULT true NOT NULL,
    "min_length" integer DEFAULT 0 NOT NULL,
    "max_length" integer DEFAULT 10000 NOT NULL,
    "placeholder" "text",
    "help_text" "text",
    "validation_schema" "jsonb",
    "options" "jsonb",
    "sort_order" integer DEFAULT 0 NOT NULL,
    "is_system" boolean DEFAULT false NOT NULL,
    "icon" "text",
    "color" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "governance_class" "text" DEFAULT 'public'::"text" NOT NULL,
    "is_protected" boolean DEFAULT false NOT NULL,
    CONSTRAINT "tools_category_check" CHECK (("category" = ANY (ARRAY['input'::"text", 'media'::"text", 'execution'::"text", 'battle'::"text", 'system'::"text"]))),
    CONSTRAINT "tools_description_check" CHECK (("char_length"("description") <= 500)),
    CONSTRAINT "tools_governance_class_check" CHECK (("governance_class" = ANY (ARRAY['public'::"text", 'internal'::"text", 'protected'::"text", 'system'::"text"]))),
    CONSTRAINT "tools_key_check" CHECK (("key" ~ '^[a-z][a-z0-9_]*$'::"text")),
    CONSTRAINT "tools_label_check" CHECK (("char_length"("label") <= 120)),
    CONSTRAINT "tools_length_order" CHECK (("max_length" >= "min_length")),
    CONSTRAINT "tools_max_length_check" CHECK (("max_length" > 0)),
    CONSTRAINT "tools_min_length_check" CHECK (("min_length" >= 0)),
    CONSTRAINT "tools_protected_class_consistency" CHECK (((NOT "is_protected") OR ("governance_class" = ANY (ARRAY['protected'::"text", 'system'::"text"])))),
    CONSTRAINT "tools_type_check" CHECK (("type" = ANY (ARRAY['text'::"text", 'textarea'::"text", 'json'::"text", 'number'::"text", 'integer'::"text", 'float'::"text", 'decimal'::"text", 'boolean'::"text", 'select'::"text", 'multiselect'::"text", 'url'::"text", 'date'::"text", 'datetime'::"text", 'file'::"text", 'execution_artifact_ids'::"text", 'battle_ids'::"text", 'media_attachment_ids'::"text"])))
);


ALTER TABLE "lenses"."tools" OWNER TO "postgres";


COMMENT ON COLUMN "lenses"."tools"."governance_class" IS 'Lens governance class. public=caller may supply; internal=registry may override; protected=registry default, override rejected; system=resolved from execution context, never visible to caller.';



COMMENT ON COLUMN "lenses"."tools"."is_protected" IS 'When true, parameter contracts referencing this tool cannot be overridden by callers (e.g. safety_filter, system_prompt). Implied by governance_class in (protected, system).';



CREATE TABLE IF NOT EXISTS "lenses"."tutorial_completions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "tutorial_slug" "text" NOT NULL,
    "kind" "text" DEFAULT 'tutorial'::"text" NOT NULL,
    "completed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "tutorial_completions_kind_check" CHECK (("kind" = ANY (ARRAY['tutorial'::"text", 'walkthrough'::"text"])))
);


ALTER TABLE "lenses"."tutorial_completions" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."tutorial_completions" IS 'CB-P1b: Server-side record of tutorial/walkthrough completions. The unique constraint ensures one-time XP per (lenser, slug, kind).';



CREATE TABLE IF NOT EXISTS "lenses"."version_parameter_contents" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "parameter_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "contents" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);

ALTER TABLE ONLY "lenses"."version_parameter_contents" FORCE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."version_parameter_contents" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."version_parameters" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "version_id" "uuid" NOT NULL,
    "label" "text" NOT NULL,
    "tool_id" "uuid" NOT NULL,
    CONSTRAINT "version_parameters_label_not_blank" CHECK (("length"(TRIM(BOTH FROM "label")) > 0))
);


ALTER TABLE "lenses"."version_parameters" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."version_parameters" IS 'Typed parameter definitions per prompt version. Parameters are owned by the version, not the prompt asset. Changing parameters requires creating a new version.';



CREATE OR REPLACE VIEW "lenses"."vw_fork_history" WITH ("security_invoker"='on') AS
 WITH RECURSIVE "fork_chain" AS (
         SELECT "l"."id" AS "lens_id",
            "l"."parent_lens_id" AS "parent_id",
            "l"."forked_from_version_id",
            "t"."title" AS "lens_title",
            1 AS "depth"
           FROM ("lenses"."lenses" "l"
             JOIN "content"."entity_translations" "t" ON ((("t"."entity_id" = "l"."id") AND ("t"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("t"."is_original" = true))))
          WHERE ("l"."parent_lens_id" IS NOT NULL)
        UNION ALL
         SELECT "fc_1"."lens_id",
            "anc"."parent_lens_id" AS "parent_id",
            "anc"."forked_from_version_id",
            "fc_1"."lens_title",
            ("fc_1"."depth" + 1)
           FROM ("fork_chain" "fc_1"
             JOIN "lenses"."lenses" "anc" ON (("anc"."id" = "fc_1"."parent_id")))
          WHERE (("anc"."parent_lens_id" IS NOT NULL) AND ("fc_1"."depth" < 20))
        )
 SELECT "fc"."lens_id",
    "fc"."parent_id" AS "forked_from_lens_id",
    "fc"."lens_title",
    "fc"."depth",
    "pt"."title" AS "forked_from_title",
    "pl"."lenser_id" AS "forked_from_lenser_id",
    "pp"."display_name" AS "forked_from_lenser_name",
    "pp"."handle" AS "forked_from_lenser_handle",
    "pp"."avatar_url" AS "forked_from_lenser_avatar_url",
    "fc"."forked_from_version_id",
    "v"."version_number" AS "forked_from_version_number"
   FROM (((("fork_chain" "fc"
     JOIN "content"."entity_translations" "pt" ON ((("pt"."entity_id" = "fc"."parent_id") AND ("pt"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("pt"."is_original" = true))))
     JOIN "lenses"."lenses" "pl" ON (("pl"."id" = "fc"."parent_id")))
     JOIN "lensers"."profiles" "pp" ON (("pp"."id" = "pl"."lenser_id")))
     LEFT JOIN "lenses"."versions" "v" ON (("v"."id" = "fc"."forked_from_version_id")));


ALTER VIEW "lenses"."vw_fork_history" OWNER TO "postgres";


COMMENT ON VIEW "lenses"."vw_fork_history" IS 'Recursive fork ancestry chain. Query by lens_id to get all ancestors ordered by depth.
   Depth 1 = immediate parent. Capped at 20 levels.
   Now includes forked_from_version_id and forked_from_version_number so the UI can
   display "Forked from LensTitle v3".';



CREATE OR REPLACE VIEW "lenses"."vw_hot_scores" AS
 SELECT "pt"."id",
    "et_orig"."language_code" AS "primary_language",
    ("log"(GREATEST((1)::numeric, (((4.0 * (COALESCE("r"."copy_count", (0)::bigint))::numeric) + (2.0 * (COALESCE("r"."like_count", (0)::bigint))::numeric)) + (1.0 * (COALESCE("r"."saved_count", (0)::bigint))::numeric)))) / "pow"(((EXTRACT(epoch FROM ("now"() - "pt"."created_at")) / 3600.0) + (2)::numeric), 1.5)) AS "hot_score"
   FROM (("lenses"."lenses" "pt"
     LEFT JOIN "content"."entity_translations" "et_orig" ON ((("et_orig"."entity_id" = "pt"."id") AND ("et_orig"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("et_orig"."is_original" = true))))
     LEFT JOIN ( SELECT "reactions"."entity_id" AS "prompt_id",
            "count"(*) FILTER (WHERE ("reactions"."reaction" = 'copy'::"content"."reaction_enum")) AS "copy_count",
            "count"(*) FILTER (WHERE ("reactions"."reaction" = 'like'::"content"."reaction_enum")) AS "like_count",
            "count"(*) FILTER (WHERE ("reactions"."reaction" = 'saved'::"content"."reaction_enum")) AS "saved_count"
           FROM "content"."reactions"
          WHERE ("reactions"."entity_type" = 'lens'::"content"."entity_type_enum")
          GROUP BY "reactions"."entity_id") "r" ON (("r"."prompt_id" = "pt"."id")))
  WHERE (("pt"."visibility" = 'public'::"content"."visibility_enum") AND ("pt"."status" = 'published'::"content"."content_status"));


ALTER VIEW "lenses"."vw_hot_scores" OWNER TO "postgres";


COMMENT ON VIEW "lenses"."vw_hot_scores" IS 'Hot-score ranking for public published lenses. Moved from content.vw_lenses_hot_scores.';



CREATE OR REPLACE VIEW "lenses"."vw_lens_version_history" AS
SELECT
    NULL::"uuid" AS "id",
    NULL::"uuid" AS "lens_id",
    NULL::integer AS "version_number",
    NULL::"text" AS "template_body",
    NULL::"content"."content_status" AS "status",
    NULL::"text" AS "changelog",
    NULL::"uuid" AS "parent_version_id",
    NULL::timestamp with time zone AS "published_at",
    NULL::timestamp with time zone AS "created_at",
    NULL::integer AS "parameter_count";


ALTER VIEW "lenses"."vw_lens_version_history" OWNER TO "postgres";


COMMENT ON VIEW "lenses"."vw_lens_version_history" IS 'All lens versions with parameter count. Replaces vw_published_versions. Use .eq(''status'', ''published'') to filter published-only.';



CREATE OR REPLACE VIEW "lenses"."vw_lenses" WITH ("security_invoker"='on') AS
 SELECT "l"."id",
    "l"."lenser_id",
    "l"."visibility",
    "l"."status",
    "l"."created_at",
    "l"."updated_at",
    "l"."parent_lens_id",
    "l"."forked_from_execution_id",
    "lv"."id" AS "latest_version_id",
    "lv"."version_number" AS "latest_version_number",
    "lv"."status" AS "latest_version_status",
    "lv"."changelog" AS "latest_changelog",
    "lv"."published_at" AS "latest_published_at",
    "t"."title",
    "t"."description",
    "t"."content",
    "t"."language_code",
    "p"."display_name" AS "author_display_name",
    "p"."handle" AS "author_handle",
    "p"."avatar_url" AS "author_avatar_url"
   FROM ((("lenses"."lenses" "l"
     LEFT JOIN LATERAL ( SELECT "v"."id",
            "v"."version_number",
            "v"."status",
            "v"."changelog",
            "v"."published_at",
            "v"."created_at"
           FROM "lenses"."versions" "v"
          WHERE (("v"."lens_id" = "l"."id") AND ("v"."status" <> 'archived'::"content"."content_status"))
          ORDER BY
                CASE "v"."status"
                    WHEN 'published'::"content"."content_status" THEN 0
                    ELSE 1
                END, "v"."version_number" DESC
         LIMIT 1) "lv" ON (true))
     LEFT JOIN "content"."entity_translations" "t" ON ((("t"."entity_id" = "l"."id") AND ("t"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("t"."is_original" = true))))
     LEFT JOIN "lensers"."profiles" "p" ON (("p"."id" = "l"."lenser_id")));


ALTER VIEW "lenses"."vw_lenses" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."workflow_edges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "source_node_id" "uuid" NOT NULL,
    "target_node_id" "uuid" NOT NULL,
    "source_output_key" "text" DEFAULT 'output'::"text" NOT NULL,
    "target_param_label" "text" NOT NULL,
    "merge_strategy" "text",
    "condition" "jsonb",
    CONSTRAINT "no_self_loops" CHECK (("source_node_id" <> "target_node_id")),
    CONSTRAINT "workflow_edges_merge_strategy_check" CHECK ((("merge_strategy" IS NULL) OR ("merge_strategy" = ANY (ARRAY['last_write_wins'::"text", 'concat'::"text", 'array'::"text", 'json_object'::"text"]))))
);


ALTER TABLE "lenses"."workflow_edges" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_edges" IS 'Directed edges in the workflow DAG. source_output_key names the field in output_data of the source node result. target_param_label names the [[label]] placeholder in the target lens template.';



COMMENT ON COLUMN "lenses"."workflow_edges"."merge_strategy" IS 'Fan-in merge strategy applied when two or more edges target the same (target_node_id, target_param_label). NULL defers to target node config.merge; default is last_write_wins.';



COMMENT ON COLUMN "lenses"."workflow_edges"."condition" IS 'Optional JSONB condition evaluated by the workflow engine. Shape: { type: "jsonpath", expr, op, value }. When condition evaluates false the edge is skipped.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_nodes" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "lens_id" "uuid",
    "version_id" "uuid",
    "position_x" double precision DEFAULT 0 NOT NULL,
    "position_y" double precision DEFAULT 0 NOT NULL,
    "label" "text",
    "ordinal" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
);


ALTER TABLE "lenses"."workflow_nodes" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_nodes" IS 'Each node represents a lens version in the workflow DAG. version_id NULL means use the lens head at execution time. position_x/y are used by the visual canvas editor.';



COMMENT ON COLUMN "lenses"."workflow_nodes"."config" IS 'Per-node execution config: { model_id?: string, param_overrides?: Record<string,string>, node_type?: "lens"|"image_generate"|"web_search"|"http_request" }';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_phases" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "title" "text" NOT NULL,
    "description" "text",
    "ordinal" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "lenses"."workflow_phases" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_phases" IS 'Ordered grouping of tasks within a workflow. Each phase represents a logical stage of execution.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_run_chains" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "parent_run_id" "uuid" NOT NULL,
    "child_run_id" "uuid" NOT NULL,
    "chain_reason" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "lenses"."workflow_run_chains" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_run_chains" IS 'CD: Links parent→child workflow runs created via chaining.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_run_dead_letters" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "node_id" "text",
    "error_code" "text",
    "error_message" "text",
    "last_attempt_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "attempt_count" integer DEFAULT 1 NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "resolved_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_lenses_workflow_run_dead_letters_attempt_count_nonneg" CHECK ((("attempt_count" IS NULL) OR ("attempt_count" >= 0)))
);


ALTER TABLE "lenses"."workflow_run_dead_letters" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."workflow_run_events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "event_id" bigint NOT NULL,
    "type" "text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workflow_run_events_type_non_empty" CHECK (("length"(TRIM(BOTH FROM "type")) > 0))
);


ALTER TABLE "lenses"."workflow_run_events" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_run_events" IS 'Append-only workflow timeline events used by SSE streaming and replay. event_id is monotonic per run_id.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_run_provenance" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "source_run_id" "uuid" NOT NULL,
    "source_node_id" "uuid" NOT NULL,
    "source_output_path" "text" NOT NULL,
    "target_run_id" "uuid" NOT NULL,
    "target_node_id" "uuid" NOT NULL,
    "target_input_path" "text" NOT NULL,
    "transform" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workflow_run_provenance_paths_non_empty" CHECK ((("length"(TRIM(BOTH FROM "source_output_path")) > 0) AND ("length"(TRIM(BOTH FROM "target_input_path")) > 0)))
);


ALTER TABLE "lenses"."workflow_run_provenance" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_run_provenance" IS 'Field-level data lineage edges between node outputs and downstream node inputs. One row per (source_field -> target_field) handoff. Spans intra-run and cross-workflow (subflow) boundaries via independent source/target run ids.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_run_tags" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "run_id" "uuid" NOT NULL,
    "tag" "text" NOT NULL,
    "severity" "text" DEFAULT 'info'::"text" NOT NULL,
    "node_id" "uuid",
    "metadata" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workflow_run_tags_severity_check" CHECK (("severity" = ANY (ARRAY['info'::"text", 'warn'::"text", 'critical'::"text"])))
);


ALTER TABLE "lenses"."workflow_run_tags" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_run_tags" IS 'Workflow-run-scoped classification tags. Mirrors execution.execution_tags but keyed by lenses.workflow_runs.id. Known tags: moderation_blocked, contract_violated, idempotent_replay, high_token_usage, partial_success, upstream_failure.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_tasks" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "phase_id" "uuid" NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "title" "text" NOT NULL,
    "prompt_text" "text",
    "output_type" "text" DEFAULT 'text'::"text" NOT NULL,
    "model_hint" "text",
    "ordinal" integer DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workflow_tasks_output_type_check" CHECK (("output_type" = ANY (ARRAY['text'::"text", 'image'::"text", 'video'::"text", 'audio'::"text", 'file'::"text"])))
);


ALTER TABLE "lenses"."workflow_tasks" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_tasks" IS 'Atomic AI step within a workflow phase. prompt_text is the paragraph instruction injected into the AI context for this task.';



COMMENT ON COLUMN "lenses"."workflow_tasks"."prompt_text" IS 'Paragraph prompt authored by the workflow owner. Injected as system instruction for this task step.';



COMMENT ON COLUMN "lenses"."workflow_tasks"."output_type" IS 'Expected output artifact kind: text | image | video | audio | file';



COMMENT ON COLUMN "lenses"."workflow_tasks"."model_hint" IS 'Optional preferred model ID for this task (overrides the workflow global model).';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_triggers" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "owner_id" "uuid" NOT NULL,
    "trigger_type" "text" NOT NULL,
    "condition" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "webhook_secret" "text",
    "enabled" boolean DEFAULT true NOT NULL,
    "last_fired_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workflow_triggers_trigger_type_check" CHECK (("trigger_type" = ANY (ARRAY['cron'::"text", 'battle_event'::"text", 'webhook'::"text", 'manual'::"text"])))
);


ALTER TABLE "lenses"."workflow_triggers" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_triggers" IS 'CD: Event-based trigger rules for workflows. Supports cron, battle_event, webhook, manual.';



CREATE TABLE IF NOT EXISTS "lenses"."workflow_version_edges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_version_id" "uuid" NOT NULL,
    "source_node_id" "uuid" NOT NULL,
    "target_node_id" "uuid" NOT NULL,
    "source_output_key" "text" DEFAULT 'output'::"text" NOT NULL,
    "target_param_label" "text" NOT NULL,
    CONSTRAINT "wve_no_self_loops" CHECK (("source_node_id" <> "target_node_id"))
);


ALTER TABLE "lenses"."workflow_version_edges" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."workflow_version_nodes" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_version_id" "uuid" NOT NULL,
    "lens_id" "uuid" NOT NULL,
    "version_id" "uuid",
    "position_x" double precision DEFAULT 0 NOT NULL,
    "position_y" double precision DEFAULT 0 NOT NULL,
    "label" "text",
    "ordinal" integer DEFAULT 0 NOT NULL,
    "config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "source_node_id" "uuid"
);


ALTER TABLE "lenses"."workflow_version_nodes" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "lenses"."workflow_versions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "version_number" integer NOT NULL,
    "changelog" "text",
    "status" "text" DEFAULT 'draft'::"text" NOT NULL,
    "published_at" timestamp with time zone,
    "created_by" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "snapshot_hash" "text",
    CONSTRAINT "workflow_versions_published_at_check" CHECK ((("status" <> 'published'::"text") OR ("published_at" IS NOT NULL))),
    CONSTRAINT "workflow_versions_status_check" CHECK (("status" = ANY (ARRAY['draft'::"text", 'published'::"text", 'archived'::"text", 'snapshot'::"text"])))
);


ALTER TABLE "lenses"."workflow_versions" OWNER TO "postgres";


COMMENT ON TABLE "lenses"."workflow_versions" IS 'Versioned snapshots of workflow DAG state. Each version freezes the nodes and edges at a point in time.';



CREATE TABLE IF NOT EXISTS "media"."attachments" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "object_id" "uuid" NOT NULL,
    "entity_type" "text" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "binding_key" "text" DEFAULT '_default'::"text" NOT NULL,
    "attached_by" "uuid",
    "attached_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "media"."attachments" OWNER TO "postgres";


COMMENT ON TABLE "media"."attachments" IS 'Binds media objects to business entities (lenses, threads, profiles, etc.) via named slots. binding_key matches prompt template placeholders.';



CREATE TABLE IF NOT EXISTS "media"."objects" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "owner_lenser_id" "uuid" NOT NULL,
    "bucket" "text",
    "object_key" "text",
    "content_text" "text",
    "external_url" "text",
    "mime_type" "text",
    "media_type" "text" NOT NULL,
    "name" "text" NOT NULL,
    "byte_size" bigint,
    "checksum_sha256" "text",
    "visibility" "text" DEFAULT 'private'::"text" NOT NULL,
    "lifecycle_state" "text" DEFAULT 'pending'::"text" NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_by" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "request_id" "uuid",
    "duration_seconds" numeric,
    "video_width" integer,
    "video_height" integer,
    "audio_sample_rate" integer,
    "audio_channels" smallint,
    "expires_at" timestamp with time zone,
    "access_count" integer DEFAULT 0 NOT NULL,
    CONSTRAINT "objects_audio_channels_range" CHECK ((("audio_channels" IS NULL) OR (("audio_channels" >= 1) AND ("audio_channels" <= 8)))),
    CONSTRAINT "objects_audio_sample_rate_positive" CHECK ((("audio_sample_rate" IS NULL) OR ("audio_sample_rate" > 0))),
    CONSTRAINT "objects_duration_positive" CHECK ((("duration_seconds" IS NULL) OR (("duration_seconds" > (0)::numeric) AND ("duration_seconds" <= (300)::numeric)))),
    CONSTRAINT "objects_lifecycle_check" CHECK (("lifecycle_state" = ANY (ARRAY['pending'::"text", 'active'::"text", 'archived'::"text", 'deleted'::"text"]))),
    CONSTRAINT "objects_media_type_check" CHECK (("media_type" = ANY (ARRAY['text'::"text", 'image'::"text", 'audio'::"text", 'video'::"text", 'document'::"text", 'json'::"text", 'binary'::"text"]))),
    CONSTRAINT "objects_one_payload" CHECK (((("bucket" IS NOT NULL) AND ("object_key" IS NOT NULL) AND ("content_text" IS NULL) AND ("external_url" IS NULL)) OR (("content_text" IS NOT NULL) AND ("bucket" IS NULL) AND ("object_key" IS NULL) AND ("external_url" IS NULL)) OR (("external_url" IS NOT NULL) AND ("bucket" IS NULL) AND ("object_key" IS NULL) AND ("content_text" IS NULL)) OR (("bucket" IS NULL) AND ("object_key" IS NULL) AND ("content_text" IS NULL) AND ("external_url" IS NULL)))),
    CONSTRAINT "objects_video_height_positive" CHECK ((("video_height" IS NULL) OR ("video_height" > 0))),
    CONSTRAINT "objects_video_width_positive" CHECK ((("video_width" IS NULL) OR ("video_width" > 0))),
    CONSTRAINT "objects_visibility_check" CHECK (("visibility" = ANY (ARRAY['public'::"text", 'private'::"text", 'unlisted'::"text"])))
);


ALTER TABLE "media"."objects" OWNER TO "postgres";


COMMENT ON TABLE "media"."objects" IS 'Canonical file/media registry. Every file, inline text, or external URL is a media object. Workspace-scoped. Replaces ai.resources.';



COMMENT ON COLUMN "media"."objects"."bucket" IS 'Supabase Storage bucket name. NULL for inline (content_text) or external (external_url) objects.';



COMMENT ON COLUMN "media"."objects"."object_key" IS 'Supabase Storage object key. Upload flow: create row → get signed URL → upload → finalize.';



COMMENT ON COLUMN "media"."objects"."mime_type" IS 'IANA MIME type. Source of truth for provider capability validation.';



COMMENT ON COLUMN "media"."objects"."media_type" IS 'High-level media category. Extensible TEXT. Values: text | image | audio | video | document | json | binary';



COMMENT ON COLUMN "media"."objects"."checksum_sha256" IS 'SHA-256 hash of the file content. Set during finalization for integrity verification.';



COMMENT ON COLUMN "media"."objects"."lifecycle_state" IS 'pending: created but upload not finalized. active: ready for use. archived: soft-archived. deleted: soft-deleted.';



COMMENT ON COLUMN "media"."objects"."duration_seconds" IS 'AN: Duration in seconds for video/audio media. NULL for images. Max 300 (5 min).';



COMMENT ON COLUMN "media"."objects"."video_width" IS 'AN: Pixel width of video/image. NULL when unknown.';



COMMENT ON COLUMN "media"."objects"."video_height" IS 'AN: Pixel height of video/image. NULL when unknown.';



COMMENT ON COLUMN "media"."objects"."audio_sample_rate" IS 'AO: Sample rate in Hz (e.g. 44100, 48000). NULL for non-audio media.';



COMMENT ON COLUMN "media"."objects"."audio_channels" IS 'AO: Number of audio channels (1=mono, 2=stereo). NULL for non-audio media.';



COMMENT ON COLUMN "media"."objects"."expires_at" IS 'AT: When set, fn_expire_media_objects will archive this object after this timestamp.';



COMMENT ON COLUMN "media"."objects"."access_count" IS 'AT: Running count of proxy-routed accesses. Incremented by fn_media_proxy_log.';



CREATE TABLE IF NOT EXISTS "organizations"."activity_log" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "org_id" "uuid" NOT NULL,
    "lenser_id" "uuid",
    "device_id" "uuid",
    "client_type" "text" DEFAULT 'web'::"text",
    "action" "text" NOT NULL,
    "resource_type" "text",
    "resource_id" "uuid",
    "payload" "jsonb" DEFAULT '{}'::"jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_activity_client_type" CHECK (("client_type" = ANY (ARRAY['web'::"text", 'mobile'::"text", 'api'::"text", 'cli'::"text"])))
);


ALTER TABLE "organizations"."activity_log" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."activity_log" IS 'Detailed activity feed for organization members. Tracks device, client type, and action payload.';



COMMENT ON COLUMN "organizations"."activity_log"."client_type" IS 'Client that generated this activity. CHECK: web, mobile, api, cli.';



CREATE TABLE IF NOT EXISTS "organizations"."addresses" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "company_id" "uuid" NOT NULL,
    "address_type" "organizations"."address_type_enum" DEFAULT 'headquarters'::"organizations"."address_type_enum" NOT NULL,
    "line1" "text" NOT NULL,
    "line2" "text",
    "postal_code" "text",
    "country_iso2_code" character(2) NOT NULL,
    "state_id" bigint,
    "city_id" bigint,
    "city_name" "text",
    "is_default" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "organizations"."addresses" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."addresses" IS 'Physical addresses for organization companies. One default per company enforced by partial unique index.';



COMMENT ON COLUMN "organizations"."addresses"."is_default" IS 'Whether this is the default address for the company. At most one default per company enforced by partial unique index.';



CREATE TABLE IF NOT EXISTS "organizations"."audit_logs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "org_id" "uuid" NOT NULL,
    "lenser_id" "uuid",
    "action" "text" NOT NULL,
    "entity_type" "text",
    "entity_id" "uuid",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb",
    "occurred_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "organizations"."audit_logs" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."audit_logs" IS 'Immutable audit trail for organization-level actions. UPDATE and DELETE are blocked by trigger.';



COMMENT ON COLUMN "organizations"."audit_logs"."action" IS 'Machine-readable action identifier (e.g. created, member_added, settings_changed).';



COMMENT ON COLUMN "organizations"."audit_logs"."entity_type" IS 'Type of entity affected (e.g. member, company, preference). Combined with entity_id for full reference.';



CREATE TABLE IF NOT EXISTS "organizations"."companies" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "org_id" "uuid" NOT NULL,
    "legal_name" character varying(180) NOT NULL,
    "trade_name" character varying(180),
    "website" "text",
    "industry" character varying(120),
    "tax_iso2_code" character(2),
    "tax_number" "text",
    "vat_id" "text",
    "registration_number" "text",
    "billing_contact_email" "text",
    "sales_contact_email" "text",
    "contact_email" "text",
    "contact_phone" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "organizations"."companies" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."companies" IS 'Legal entity details for an organization. One company per org (unique org_id).';



COMMENT ON COLUMN "organizations"."companies"."tax_iso2_code" IS 'Country of tax registration. FK to core.countries(iso2).';



COMMENT ON COLUMN "organizations"."companies"."vat_id" IS 'EU VAT identification number or equivalent. NULL if not applicable.';



CREATE TABLE IF NOT EXISTS "organizations"."preferences" (
    "org_id" "uuid" NOT NULL,
    "iso2_code" character(2),
    "timezone" character varying(120) DEFAULT 'UTC'::character varying,
    "preferred_language" "text" DEFAULT 'en'::"text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_organizations_preferences_preferred_language_bcp47" CHECK ("i18n"."fn_is_supported_language"("preferred_language"))
);


ALTER TABLE "organizations"."preferences" OWNER TO "postgres";


COMMENT ON TABLE "organizations"."preferences" IS 'Per-org locale and regional preferences. One row per organization.';



COMMENT ON COLUMN "organizations"."preferences"."iso2_code" IS 'Country code for regional defaults (date format, currency display). FK to core.countries(iso2).';



CREATE TABLE IF NOT EXISTS "platform"."api_worker_heartbeats" (
    "worker_id" "text" NOT NULL,
    "worker_type" "text" DEFAULT 'workflow'::"text" NOT NULL,
    "last_seen_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
);


ALTER TABLE "platform"."api_worker_heartbeats" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "platform"."system_flags" (
    "key" "text" NOT NULL,
    "value" "jsonb" NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_by" "uuid"
);


ALTER TABLE "platform"."system_flags" OWNER TO "postgres";


COMMENT ON TABLE "platform"."system_flags" IS 'Platform-level feature/kill-switch flags. Keyed by name, value is jsonb. service_role has full access; authenticated users have read-only access.';



CREATE TABLE IF NOT EXISTS "platform"."tool_invocation_logs" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "ai_lenser_id" "uuid" NOT NULL,
    "run_id" "uuid",
    "tool_name" "text" NOT NULL,
    "tool_input" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "tool_output" "jsonb",
    "approval_status" "text" DEFAULT 'auto_approved'::"text" NOT NULL,
    "invoked_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "decided_at" timestamp with time zone,
    "decided_by" "uuid",
    CONSTRAINT "tool_invocation_logs_approval_status_check" CHECK (("approval_status" = ANY (ARRAY['auto_approved'::"text", 'awaiting_approval'::"text", 'approved'::"text", 'rejected'::"text"])))
);


ALTER TABLE "platform"."tool_invocation_logs" OWNER TO "postgres";


COMMENT ON TABLE "platform"."tool_invocation_logs" IS 'Audit log of tool invocations by ai_lensers during workflow runs. RLS: owner SELECT only; writes go through fn_decide_tool_invocation.';



CREATE OR REPLACE VIEW "public"."contact_messages" WITH ("security_invoker"='on') AS
 SELECT "id",
    "lenser_id",
    "name",
    "email",
    "subject",
    "message",
    "kvkk_approved",
    "created_at",
    "ip_address",
    "user_agent"
   FROM "analytics"."contact";


ALTER VIEW "public"."contact_messages" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."notification_aggregates" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "recipient_id" "uuid" NOT NULL,
    "notification_type" "text" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "window_start" timestamp with time zone DEFAULT "now"() NOT NULL,
    "window_end" timestamp with time zone NOT NULL,
    "actor_ids" "uuid"[] DEFAULT '{}'::"uuid"[] NOT NULL,
    "notification_id" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "public"."notification_aggregates" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."notification_preferences" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "notification_type" "text" NOT NULL,
    "enabled" boolean DEFAULT true NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "public"."notification_preferences" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."notifications" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "type" "text" NOT NULL,
    "title" "text" NOT NULL,
    "body" "text",
    "action_url" "text",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "read_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "notifications_type_check" CHECK (("type" = ANY (ARRAY['battle_result'::"text", 'battle_started'::"text", 'vote_reminder'::"text", 'template_battle_open'::"text", 'template_battle_published'::"text", 'vote_received'::"text", 'battle_assigned'::"text", 'battle_vote_cast'::"text", 'battle_joined'::"text", 'battle_won'::"text", 'battle_lost'::"text", 'battle_comment'::"text", 'follow_new'::"text", 'follow_request'::"text", 'follow_accepted'::"text", 'lens_reaction'::"text", 'lens_comment'::"text", 'lens_published'::"text", 'lens_forked'::"text", 'lens_featured'::"text", 'lens_milestone'::"text", 'workflow_published'::"text", 'workflow_forked'::"text", 'agent_update'::"text", 'agent_cron_result'::"text", 'agent_critical'::"text", 'agent_created'::"text", 'agent_battle_won'::"text", 'team_run_started'::"text", 'team_run_completed'::"text", 'team_run_failed'::"text", 'cron_run_completed'::"text", 'cron_run_failed'::"text", 'policy_updated'::"text", 'model_binding_changed'::"text", 'requirement_update'::"text", 'badge_awarded'::"text", 'leaderboard_change'::"text", 'system'::"text"])))
);


ALTER TABLE "public"."notifications" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."user_oauth_connections" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "provider" "text" NOT NULL,
    "capability" "text" NOT NULL,
    "connection_label" "text" NOT NULL,
    "ref" "text" NOT NULL,
    "auth_strategy" "text" DEFAULT 'oauth2'::"text" NOT NULL,
    "provider_config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "access_token_id" "uuid" NOT NULL,
    "refresh_token_id" "uuid",
    "granted_scopes" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "expires_at" timestamp with time zone,
    "is_active" boolean DEFAULT true NOT NULL,
    "revoked_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "uoc_auth_strategy_check" CHECK (("auth_strategy" = ANY (ARRAY['oauth2'::"text", 'api_key'::"text", 'webhook'::"text", 'none'::"text"]))),
    CONSTRAINT "uoc_capability_check" CHECK (("capability" = ANY (ARRAY['database'::"text", 'page'::"text", 'gmail'::"text", 'drive'::"text", 'sheets'::"text", 'docs'::"text", 'calendar'::"text", 'tasks'::"text", 'boards'::"text", 'webhooks'::"text", 'chat'::"text", 'repos'::"text", 'issues'::"text", 'projects'::"text", 'lists'::"text", 'records'::"text", 'crm'::"text", 'messages'::"text", 'channels'::"text", 'files'::"text", 'events'::"text", 'http'::"text"]))),
    CONSTRAINT "uoc_label_format" CHECK (("connection_label" ~ '^[a-z0-9][a-z0-9_-]{0,47}$'::"text")),
    CONSTRAINT "uoc_provider_check" CHECK (("provider" = ANY (ARRAY['notion'::"text", 'google'::"text", 'asana'::"text", 'monday'::"text", 'zapier'::"text", 'slack'::"text", 'github'::"text", 'gitlab'::"text", 'jira'::"text", 'linear'::"text", 'trello'::"text", 'airtable'::"text", 'hubspot'::"text", 'salesforce'::"text", 'discord'::"text", 'microsoft_teams'::"text", 'microsoft_outlook'::"text", 'microsoft_onedrive'::"text", 'microsoft_excel'::"text", 'dropbox'::"text", 'box'::"text", 'calendly'::"text", 'clickup'::"text", 'todoist'::"text", 'custom_http'::"text"]))),
    CONSTRAINT "uoc_ref_format" CHECK ((("ref" ~ '^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z0-9][a-z0-9_-]{0,47}$'::"text") AND ("char_length"("ref") <= 120))),
    CONSTRAINT "uoc_revoke_consistency" CHECK (((("is_active" = true) AND ("revoked_at" IS NULL)) OR (("is_active" = false) AND ("revoked_at" IS NOT NULL))))
);


ALTER TABLE "public"."user_oauth_connections" OWNER TO "postgres";


COMMENT ON TABLE "public"."user_oauth_connections" IS 'Per-user OAuth connections to external providers. Tokens stored in Supabase Vault (never in this table). The ref column is the stable identifier for [[:connector:ref]] expression syntax. All writes go through SECURITY DEFINER RPCs; direct INSERT/UPDATE/DELETE is revoked from the authenticated role.';



COMMENT ON COLUMN "public"."user_oauth_connections"."ref" IS 'Stable connector reference slug, e.g. ''google.gmail.primary''. Used as the key in [[:connector:google.gmail.primary]] expressions.';



COMMENT ON COLUMN "public"."user_oauth_connections"."access_token_id" IS 'vault.secrets.id for the OAuth access token. Decrypted only server-side.';



COMMENT ON COLUMN "public"."user_oauth_connections"."refresh_token_id" IS 'vault.secrets.id for the OAuth refresh token. Null if provider does not issue one.';



CREATE OR REPLACE VIEW "public"."v_workflow_run_cost_breakdown" WITH ("security_invoker"='on') AS
 SELECT "r"."id" AS "run_id",
    "r"."workflow_id",
    "r"."status" AS "run_status",
    "r"."triggered_by",
    "count"("nr"."id") AS "total_nodes",
    "count"("nr"."id") FILTER (WHERE ("nr"."status" = 'completed'::"text")) AS "completed_nodes",
    "count"("nr"."id") FILTER (WHERE ("nr"."status" = ANY (ARRAY['failed'::"text", 'timed_out'::"text", 'invalidated'::"text", 'blocked'::"text"]))) AS "failed_nodes",
    "count"("nr"."id") FILTER (WHERE ("nr"."status" = 'skipped'::"text")) AS "skipped_nodes",
    COALESCE("sum"("nr"."input_tokens"), (0)::bigint) AS "total_input_tokens",
    COALESCE("sum"("nr"."output_tokens"), (0)::bigint) AS "total_output_tokens",
    COALESCE("sum"("nr"."cost_credits"), (0)::bigint) AS "total_cost_credits",
    COALESCE("sum"("nr"."retry_count"), (0)::bigint) AS "total_retries",
    (COALESCE("avg"("nr"."duration_ms"), (0)::numeric))::integer AS "avg_duration_ms",
    COALESCE("max"("nr"."duration_ms"), 0) AS "max_duration_ms",
    (COALESCE("avg"("nr"."ttfb_ms"), (0)::numeric))::integer AS "avg_ttfb_ms"
   FROM ("lenses"."workflow_runs" "r"
     LEFT JOIN "lenses"."workflow_node_results" "nr" ON (("nr"."run_id" = "r"."id")))
  GROUP BY "r"."id", "r"."workflow_id", "r"."status", "r"."triggered_by";


ALTER VIEW "public"."v_workflow_run_cost_breakdown" OWNER TO "postgres";


COMMENT ON VIEW "public"."v_workflow_run_cost_breakdown" IS 'Phase 6 — Per-run token + credit + latency rollup. Drives cost dashboards and per-lenser usage reports.';



CREATE OR REPLACE VIEW "public"."v_workflow_run_health" WITH ("security_invoker"='on') AS
 SELECT "id" AS "run_id",
    "workflow_id",
    "status",
    "run_worker_id",
    "heartbeat_at",
    "started_at",
    "completed_at",
    "parent_run_id",
    "recursion_depth",
    EXTRACT(epoch FROM ("now"() - COALESCE("heartbeat_at", "started_at"))) AS "seconds_since_heartbeat",
        CASE
            WHEN ("status" = ANY (ARRAY['completed'::"text", 'failed'::"text", 'cancelled'::"text", 'timed_out'::"text"])) THEN 'terminal'::"text"
            WHEN ("heartbeat_at" IS NULL) THEN 'no_heartbeat'::"text"
            WHEN (("now"() - "heartbeat_at") > '00:01:00'::interval) THEN 'stale'::"text"
            ELSE 'live'::"text"
        END AS "liveness",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_node_results" "nr"
          WHERE (("nr"."run_id" = "r"."id") AND ("nr"."status" = ANY (ARRAY['pending'::"text", 'awaiting_dependency'::"text", 'queued'::"text"])))) AS "pending_nodes",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_node_results" "nr"
          WHERE (("nr"."run_id" = "r"."id") AND ("nr"."status" = ANY (ARRAY['running'::"text", 'streaming'::"text", 'retrying'::"text"])))) AS "in_flight_nodes",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_node_results" "nr"
          WHERE (("nr"."run_id" = "r"."id") AND ("nr"."status" = ANY (ARRAY['completed'::"text", 'skipped'::"text"])))) AS "done_nodes"
   FROM "lenses"."workflow_runs" "r";


ALTER VIEW "public"."v_workflow_run_health" OWNER TO "postgres";


COMMENT ON VIEW "public"."v_workflow_run_health" IS 'Phase 6 + 7 — Liveness + progress signals per run. `liveness` maps to recovery actions: live = leave alone, stale = candidate for fn_claim_stale_workflow_run, no_heartbeat = never advanced past queued, terminal = finished.';



CREATE OR REPLACE VIEW "public"."v_workflow_run_timeline" WITH ("security_invoker"='on') AS
 SELECT "r"."id" AS "run_id",
    "r"."workflow_id",
    "r"."status" AS "run_status",
    "r"."started_at" AS "run_started_at",
    "nr"."node_id",
    "n"."label" AS "node_label",
    "nr"."status" AS "node_status",
    "nr"."started_at" AS "node_started_at",
    "nr"."completed_at" AS "node_completed_at",
    "nr"."retry_count",
    "nr"."duration_ms",
    "nr"."ttfb_ms",
    "nr"."error_message",
    EXTRACT(epoch FROM ("nr"."started_at" - "r"."started_at")) AS "rel_start_s",
    EXTRACT(epoch FROM ("nr"."completed_at" - "r"."started_at")) AS "rel_end_s"
   FROM (("lenses"."workflow_runs" "r"
     JOIN "lenses"."workflow_node_results" "nr" ON (("nr"."run_id" = "r"."id")))
     LEFT JOIN "lenses"."workflow_nodes" "n" ON (("n"."id" = "nr"."node_id")))
  ORDER BY "r"."id", "nr"."started_at", "nr"."node_id";


ALTER VIEW "public"."v_workflow_run_timeline" OWNER TO "postgres";


COMMENT ON VIEW "public"."v_workflow_run_timeline" IS 'Phase 6 — Chronological timeline of node execution within a run. Used by the monitoring UI to render Gantt-style lanes.';



CREATE OR REPLACE VIEW "public"."vw_ai_models_public" AS
 SELECT "m"."id",
    "m"."name",
    "m"."key",
    "p"."key" AS "provider_key",
    "p"."display_name" AS "provider_name",
    "m"."context_window_tokens",
    "m"."supports_tools",
    "m"."supports_json_schema",
    "m"."supports_vision",
    "m"."capabilities",
    "m"."input_modalities",
    "m"."output_modalities",
    "m"."is_active"
   FROM ("ai"."models" "m"
     JOIN "ai"."providers" "p" ON (("p"."id" = "m"."provider_id")))
  WHERE (("m"."is_active" = true) AND ("p"."is_active" = true))
  ORDER BY "p"."display_name", "m"."name";


ALTER VIEW "public"."vw_ai_models_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_auth_lenser" WITH ("security_invoker"='on') AS
 SELECT "u"."id" AS "user_id",
    "l"."id" AS "lenser_id"
   FROM ("auth"."users" "u"
     JOIN "lensers"."profiles" "l" ON (("l"."user_id" = "u"."id")));


ALTER VIEW "public"."vw_auth_lenser" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battle_funnel" WITH ("security_invoker"='on') AS
 WITH "status_counts" AS (
         SELECT "count"(*) AS "total",
            "count"(*) FILTER (WHERE ("battles"."status" <> 'draft'::"battles"."battle_status_enum")) AS "past_draft",
            "count"(*) FILTER (WHERE ("battles"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"]))) AS "reached_voting",
            "count"(*) FILTER (WHERE ("battles"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"]))) AS "reached_closed",
            "count"(*) FILTER (WHERE ("battles"."status" = ANY (ARRAY['published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"]))) AS "reached_published",
            "count"(*) FILTER (WHERE ("battles"."status" = 'archived'::"battles"."battle_status_enum")) AS "reached_archived"
           FROM "battles"."battles"
          WHERE ("battles"."deleted_at" IS NULL)
        )
 SELECT "total",
    "past_draft",
    "reached_voting",
    "reached_closed",
    "reached_published",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("past_draft")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_past_draft",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("reached_voting")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_reached_voting",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("reached_closed")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_reached_closed",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("reached_published")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_reached_published"
   FROM "status_counts";


ALTER VIEW "public"."vw_battle_funnel" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battle_health" WITH ("security_invoker"='on') AS
 SELECT "b"."id",
    "b"."title",
    "b"."slug",
    "b"."status",
    "b"."created_at",
    "b"."voting_opens_at",
    "b"."voting_closes_at",
    "b"."finalized_at",
    "b"."published_at",
    "b"."total_vote_count",
        CASE
            WHEN ("b"."total_vote_count" >= 5) THEN 'confident'::"text"
            WHEN ("b"."total_vote_count" >= 3) THEN 'moderate'::"text"
            ELSE 'low'::"text"
        END AS "confidence_level",
    "b"."winner_contender_id",
    "cc"."contender_count",
    "sc"."submission_count",
        CASE
            WHEN (("b"."finalized_at" IS NOT NULL) AND ("b"."created_at" IS NOT NULL)) THEN (EXTRACT(epoch FROM ("b"."finalized_at" - "b"."created_at")) / 3600.0)
            ELSE NULL::numeric
        END AS "hours_to_finalize"
   FROM (("battles"."battles" "b"
     LEFT JOIN LATERAL ( SELECT "count"(*) AS "contender_count"
           FROM "battles"."contenders" "c"
          WHERE ("c"."battle_id" = "b"."id")) "cc" ON (true))
     LEFT JOIN LATERAL ( SELECT "count"(*) AS "submission_count"
           FROM "battles"."submissions" "s"
          WHERE (("s"."battle_id" = "b"."id") AND ("s"."status" = 'submitted'::"battles"."submission_status_enum"))) "sc" ON (true))
  WHERE ("b"."deleted_at" IS NULL);


ALTER VIEW "public"."vw_battle_health" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battle_participation" AS
 WITH "weekly_battles" AS (
         SELECT "date_trunc"('week'::"text", "b"."created_at") AS "week",
            "count"(DISTINCT "b"."id") AS "battles_created",
            "count"(DISTINCT
                CASE
                    WHEN ("b"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) THEN "b"."id"
                    ELSE NULL::"uuid"
                END) AS "battles_completed",
            "count"(DISTINCT "b"."creator_lenser_id") AS "unique_hosts"
           FROM "battles"."battles" "b"
          WHERE ("b"."deleted_at" IS NULL)
          GROUP BY ("date_trunc"('week'::"text", "b"."created_at"))
        ), "weekly_votes" AS (
         SELECT "date_trunc"('week'::"text", "b"."created_at") AS "week",
            "count"(DISTINCT "v"."voter_lenser_id") AS "unique_voters",
            "count"(*) AS "total_votes"
           FROM ("battles"."votes" "v"
             JOIN "battles"."battles" "b" ON (("b"."id" = "v"."battle_id")))
          WHERE ("b"."deleted_at" IS NULL)
          GROUP BY ("date_trunc"('week'::"text", "b"."created_at"))
        )
 SELECT "wb"."week",
    "wb"."battles_created",
    "wb"."battles_completed",
    "wb"."unique_hosts",
    COALESCE("wv"."unique_voters", (0)::bigint) AS "unique_voters",
    COALESCE("wv"."total_votes", (0)::bigint) AS "total_votes"
   FROM ("weekly_battles" "wb"
     LEFT JOIN "weekly_votes" "wv" ON (("wv"."week" = "wb"."week")))
  ORDER BY "wb"."week" DESC;


ALTER VIEW "public"."vw_battle_participation" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battles_public" AS
 SELECT "b"."id",
    "b"."title",
    "b"."slug",
    "b"."status",
    "b"."creator_lenser_id",
    "b"."total_vote_count",
    "b"."created_at",
    "b"."updated_at",
    "cc"."contender_count"
   FROM ("battles"."battles" "b"
     LEFT JOIN LATERAL ( SELECT "count"(*) AS "contender_count"
           FROM "battles"."contenders" "c"
          WHERE ("c"."battle_id" = "b"."id")) "cc" ON (true))
  WHERE (("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL));


ALTER VIEW "public"."vw_battles_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_content_tags_public" AS
 SELECT "t"."id",
    "t"."slug",
    COALESCE("tn"."name", "t"."slug") AS "name",
    "t"."visibility"
   FROM ("content"."tags" "t"
     LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
           FROM "content"."tag_translations"
          WHERE ("tag_translations"."tag_id" = "t"."id")
         LIMIT 1) "tn" ON (true))
  WHERE ("t"."visibility" = 'public'::"content"."tag_visibility_enum");


ALTER VIEW "public"."vw_content_tags_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_content_thread_replies_public" AS
 SELECT "r"."id",
    "r"."thread_id",
    "r"."parent_reply_id",
    "r"."lenser_id",
    "r"."content",
    "r"."content_html",
    "rt"."reaction_totals",
    "r"."created_at",
    "jsonb_build_object"('handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile"
   FROM ((("content"."thread_replies" "r"
     JOIN "content"."threads" "t" ON (("t"."id" = "r"."thread_id")))
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "r"."lenser_id")))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals"
           FROM ( SELECT "rx"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "rx"
                  WHERE (("rx"."entity_type" = 'thread_reply'::"content"."entity_type_enum") AND ("rx"."entity_id" = "r"."id"))
                  GROUP BY "rx"."reaction") "x") "rt" ON (true))
  WHERE (("t"."visibility" = 'public'::"content"."visibility_enum") AND ("t"."status" = 'published'::"content"."content_status") AND ("r"."status" = 'published'::"content"."thread_reply_status") AND ("r"."deleted_at" IS NULL))
  ORDER BY "r"."created_at";


ALTER VIEW "public"."vw_content_thread_replies_public" OWNER TO "postgres";


COMMENT ON VIEW "public"."vw_content_thread_replies_public" IS 'Public view of published thread replies with reaction aggregates. Uses unified content.reactions table (updated in 20260440000012).';



CREATE OR REPLACE VIEW "public"."vw_content_threads_public" AS
 SELECT "t"."id",
    "t"."lenser_id",
    "prof"."handle" AS "lenser_handle",
    COALESCE("et"."title", 'Untitled'::"text") AS "title",
    COALESCE("et"."content", ''::"text") AS "content",
    "jsonb_build_object"('handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile",
    "rt"."reaction_totals",
    "rt"."like_count",
    "t"."reply_count",
    "t"."view_count",
    "t"."created_at",
    "t"."thumbnail_url",
    "t"."lens_data",
    "t"."visibility",
    "tg_agg"."tags"
   FROM (((("content"."threads" "t"
     LEFT JOIN "content"."entity_translations" "et" ON ((("et"."entity_id" = "t"."id") AND ("et"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("et"."is_original" = true))))
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "t"."lenser_id")))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'like'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "like_count"
           FROM ( SELECT "rx"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "rx"
                  WHERE (("rx"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("rx"."entity_id" = "t"."id"))
                  GROUP BY "rx"."reaction") "x") "rt" ON (true))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_agg"("jsonb_build_object"('id', "tg"."id", 'slug', "tg"."slug", 'name', COALESCE("tn"."name", "tg"."slug"))), '[]'::"jsonb") AS "tags"
           FROM (("content"."tag_map" "tm"
             JOIN "content"."tags" "tg" ON (("tg"."id" = "tm"."tag_id")))
             LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
                   FROM "content"."tag_translations"
                  WHERE ("tag_translations"."tag_id" = "tg"."id")
                 LIMIT 1) "tn" ON (true))
          WHERE (("tm"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("tm"."entity_id" = "t"."id"))) "tg_agg" ON (true))
  WHERE (("t"."visibility" = 'public'::"content"."visibility_enum") AND ("t"."status" = 'published'::"content"."content_status"));


ALTER VIEW "public"."vw_content_threads_public" OWNER TO "postgres";


COMMENT ON VIEW "public"."vw_content_threads_public" IS 'Public view of published threads with reaction aggregates. Uses unified content.reactions and content.entity_translations tables (updated in 20260440000012).';



COMMENT ON COLUMN "public"."vw_content_threads_public"."lenser_handle" IS 'Flat text copy of the author handle. Use eq(lenser_handle, ''foo'') for profile-page filtering — this is indexable, unlike author_profile->>(''handle'').';



CREATE OR REPLACE VIEW "public"."vw_feedback_admin" WITH ("security_invoker"='on') AS
 SELECT "id",
    "product_tag",
    "page",
    "user_id",
    "message",
    "start_date",
    "end_date",
    "created_at",
    "status"
   FROM "analytics"."product_feedback";


ALTER VIEW "public"."vw_feedback_admin" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_feedback_user" AS
 SELECT "product_tag",
    "page",
    "message",
    "start_date",
    "end_date",
    "status",
    "created_at"
   FROM "analytics"."product_feedback"
  WHERE ("user_id" = "auth"."uid"());


ALTER VIEW "public"."vw_feedback_user" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_global_messages" AS
 SELECT "id",
    "battle_id",
    "sender_id",
    "sender_handle",
    "sender_role",
    "body",
    "created_at"
   FROM "battles"."global_messages";


ALTER VIEW "public"."vw_global_messages" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."levels" (
    "id" integer NOT NULL,
    "app_id" "uuid" NOT NULL,
    "level" integer NOT NULL,
    "min_total_xp" bigint NOT NULL,
    "max_total_xp" bigint NOT NULL,
    "metadata" "jsonb"
);


ALTER TABLE "xp"."levels" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lensers_public_recent" AS
 SELECT "l"."handle",
    "l"."display_name",
    "l"."avatar_url",
    "l"."headline",
    "l"."status",
    "l"."created_at",
    "t"."lenser_id",
    "t"."total_xp",
    "t"."current_level",
    "t"."app_id",
    "lv"."min_total_xp" AS "min_xp",
    "lv"."max_total_xp" AS "max_xp",
    "jl"."join_order",
    "jl"."joined_at",
    "s"."thread_count",
    "s"."lens_count",
    "s"."follower_count",
    "s"."following_count"
   FROM (((("lensers"."profiles" "l"
     LEFT JOIN "xp"."totals" "t" ON (("t"."lenser_id" = "l"."id")))
     LEFT JOIN "xp"."levels" "lv" ON ((("lv"."app_id" = "t"."app_id") AND ("lv"."level" = "t"."current_level"))))
     LEFT JOIN "analytics"."lenser_join_log" "jl" ON (("jl"."lenser_id" = "l"."id")))
     LEFT JOIN "analytics"."lenser_stats" "s" ON (("s"."lenser_id" = "l"."id")))
  WHERE (("l"."status" = 'active'::"lensers"."lenser_status") AND ("l"."deletion_requested_at" IS NULL))
  ORDER BY "l"."created_at" DESC
 LIMIT 5;


ALTER VIEW "public"."vw_lensers_public_recent" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lensers_social_links_private" WITH ("security_invoker"='on') AS
 SELECT "p"."handle",
    "l"."id",
    "l"."platform",
    "l"."url",
    "l"."label",
    "l"."created_at"
   FROM ("lensers"."profiles" "p"
     JOIN "lensers"."social_links" "l" ON (("l"."lenser_id" = "p"."id")))
  WHERE ("p"."user_id" = "auth"."uid"());


ALTER VIEW "public"."vw_lensers_social_links_private" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lensers_social_links_public" AS
 SELECT "p"."handle",
    "l"."platform",
    "l"."url",
    "l"."label"
   FROM ("lensers"."profiles" "p"
     JOIN "lensers"."social_links" "l" ON (("l"."lenser_id" = "p"."id")))
  WHERE (("p"."status" = 'active'::"lensers"."lenser_status") AND ("p"."deletion_requested_at" IS NULL) AND ("p"."visibility" = 'public'::"lensers"."lenser_visibility"));


ALTER VIEW "public"."vw_lensers_social_links_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lenses_public" AS
 SELECT "pt"."id",
    "pt"."lenser_id",
    "prof"."handle" AS "lenser_handle",
    "pt"."visibility",
    COALESCE("et"."title", 'Untitled'::"text") AS "title",
    "et"."description",
    COALESCE("et"."content", ''::"text") AS "content",
    "jsonb_build_object"('id', "prof"."id", 'handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile",
    "rt"."reaction_totals",
    "rt"."copy_count",
    "rt"."like_count",
    "rt"."saved_count",
    "pt"."created_at",
    "tg_agg"."tags"
   FROM (((("lenses"."lenses" "pt"
     LEFT JOIN "content"."entity_translations" "et" ON ((("et"."entity_id" = "pt"."id") AND ("et"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("et"."is_original" = true))))
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "pt"."lenser_id")))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'copy'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "copy_count",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'like'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "like_count",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'saved'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "saved_count"
           FROM ( SELECT "rx"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "rx"
                  WHERE (("rx"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("rx"."entity_id" = "pt"."id"))
                  GROUP BY "rx"."reaction") "x") "rt" ON (true))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_agg"("jsonb_build_object"('id', "tg"."id", 'slug', "tg"."slug", 'name', COALESCE("tn"."name", "tg"."slug"))), '[]'::"jsonb") AS "tags"
           FROM (("content"."tag_map" "tm"
             JOIN "content"."tags" "tg" ON (("tg"."id" = "tm"."tag_id")))
             LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
                   FROM "content"."tag_translations"
                  WHERE ("tag_translations"."tag_id" = "tg"."id")
                 LIMIT 1) "tn" ON (true))
          WHERE (("tm"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("tm"."entity_id" = "pt"."id"))) "tg_agg" ON (true))
  WHERE (("pt"."visibility" = 'public'::"content"."visibility_enum") AND ("pt"."status" = 'published'::"content"."content_status"));


ALTER VIEW "public"."vw_lenses_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_tags_public_extended" AS
 SELECT "t"."id",
    "t"."slug",
    COALESCE("tn"."name", "t"."slug") AS "name",
    "t"."created_at",
    'public'::"text" AS "visibility",
    (0)::bigint AS "created_count",
    (0)::bigint AS "viewed_count",
    (0)::bigint AS "reacted_count",
    (0)::bigint AS "total_usage",
    (0)::bigint AS "trend_score"
   FROM ("content"."tags" "t"
     LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
           FROM "content"."tag_translations"
          WHERE ("tag_translations"."tag_id" = "t"."id")
         LIMIT 1) "tn" ON (true))
  WHERE ("t"."visibility" = 'public'::"content"."tag_visibility_enum");


ALTER VIEW "public"."vw_tags_public_extended" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_tags_public_stats" AS
 WITH "events_filtered" AS (
         SELECT "e"."tag_id",
            "e"."activity_type",
            ("e"."occurred_at")::"date" AS "activity_date"
           FROM ((("analytics"."tag_activity_events" "e"
             JOIN "content"."tags" "t_1" ON ((("t_1"."id" = "e"."tag_id") AND ("t_1"."visibility" = 'public'::"content"."tag_visibility_enum"))))
             LEFT JOIN "lenses"."lenses" "p" ON ((("e"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("e"."entity_id" = "p"."id"))))
             LEFT JOIN "content"."threads" "th" ON ((("e"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("e"."entity_id" = "th"."id"))))
          WHERE ((("e"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("p"."visibility" = 'public'::"content"."visibility_enum")) OR (("e"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("th"."visibility" = 'public'::"content"."visibility_enum")))
        ), "lifetime" AS (
         SELECT "events_filtered"."tag_id",
            "count"(*) FILTER (WHERE ("events_filtered"."activity_type" = 'created'::"text")) AS "created_count",
            "count"(*) FILTER (WHERE ("events_filtered"."activity_type" = 'viewed'::"text")) AS "viewed_count",
            "count"(*) FILTER (WHERE ("events_filtered"."activity_type" = 'reacted'::"text")) AS "reacted_count"
           FROM "events_filtered"
          GROUP BY "events_filtered"."tag_id"
        ), "recent_7d" AS (
         SELECT "events_filtered"."tag_id",
            "sum"(((
                CASE
                    WHEN ("events_filtered"."activity_type" = 'created'::"text") THEN 1
                    ELSE 0
                END +
                CASE
                    WHEN ("events_filtered"."activity_type" = 'viewed'::"text") THEN 2
                    ELSE 0
                END) +
                CASE
                    WHEN ("events_filtered"."activity_type" = 'reacted'::"text") THEN 3
                    ELSE 0
                END)) AS "trend_score_7d"
           FROM "events_filtered"
          WHERE ("events_filtered"."activity_date" >= (CURRENT_DATE - '7 days'::interval))
          GROUP BY "events_filtered"."tag_id"
        )
 SELECT "t"."id",
    "t"."slug",
    COALESCE("tn"."name", "t"."slug") AS "name",
    "t"."created_at",
    'public'::"text" AS "visibility",
    COALESCE("l"."created_count", (0)::bigint) AS "created_count",
    COALESCE("l"."viewed_count", (0)::bigint) AS "viewed_count",
    COALESCE("l"."reacted_count", (0)::bigint) AS "reacted_count",
    ((COALESCE("l"."created_count", (0)::bigint) + COALESCE("l"."viewed_count", (0)::bigint)) + COALESCE("l"."reacted_count", (0)::bigint)) AS "total_usage",
    COALESCE("r"."trend_score_7d", (0)::bigint) AS "trend_score_7d"
   FROM ((("content"."tags" "t"
     LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
           FROM "content"."tag_translations"
          WHERE ("tag_translations"."tag_id" = "t"."id")
         LIMIT 1) "tn" ON (true))
     LEFT JOIN "lifetime" "l" ON (("l"."tag_id" = "t"."id")))
     LEFT JOIN "recent_7d" "r" ON (("r"."tag_id" = "t"."id")))
  WHERE ("t"."visibility" = 'public'::"content"."tag_visibility_enum");


ALTER VIEW "public"."vw_tags_public_stats" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_workflows" AS
 SELECT "id",
    "lenser_id",
    "title",
    "description",
    "visibility",
    "battle_count",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_nodes" "wn"
          WHERE ("wn"."workflow_id" = "w"."id")) AS "node_count",
    "reaction_totals",
    "fork_count",
    "parent_workflow_id",
    "created_at",
    "updated_at"
   FROM "lenses"."workflows" "w";


ALTER VIEW "public"."vw_workflows" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_xp_leaderboard_global" AS
 WITH "aggregated" AS (
         SELECT "t"."lenser_id",
            "sum"("t"."total_xp") AS "total_xp",
            "max"("t"."current_level") AS "current_level"
           FROM "xp"."totals" "t"
          GROUP BY "t"."lenser_id"
        ), "ranked" AS (
         SELECT "a"."lenser_id",
            "a"."total_xp",
            "a"."current_level",
            "rank"() OVER (ORDER BY "a"."total_xp" DESC, "a"."lenser_id") AS "rank"
           FROM "aggregated" "a"
        )
 SELECT "r"."rank",
    "r"."lenser_id",
    "r"."total_xp",
    "r"."current_level",
    "jsonb_build_object"('display_name', "l"."display_name", 'handle', "l"."handle", 'avatar_url', "l"."avatar_url") AS "user"
   FROM ("ranked" "r"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "r"."lenser_id")))
  WHERE ("r"."rank" <= 100);


ALTER VIEW "public"."vw_xp_leaderboard_global" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."season_totals" (
    "season_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "app_id" "uuid" NOT NULL,
    "total_xp" bigint DEFAULT 0 NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_xp_season_totals_total_xp_nonneg" CHECK ((("total_xp" IS NULL) OR ("total_xp" >= 0)))
);


ALTER TABLE "xp"."season_totals" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."seasons" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "app_id" "uuid" NOT NULL,
    "slug" "text" NOT NULL,
    "name" "text",
    "starts_at" timestamp with time zone NOT NULL,
    "ends_at" timestamp with time zone NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "description" "text",
    "reward_description" "text",
    "featured_challenges" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
    CONSTRAINT "chk_season_date_order" CHECK (("starts_at" < "ends_at"))
);


ALTER TABLE "xp"."seasons" OWNER TO "postgres";


COMMENT ON COLUMN "xp"."seasons"."description" IS 'CB: Human-readable season narrative shown in the season dashboard.';



COMMENT ON COLUMN "xp"."seasons"."reward_description" IS 'CB: Description of seasonal rewards (badges, titles, bonuses).';



COMMENT ON COLUMN "xp"."seasons"."featured_challenges" IS 'CB: Array of {title, description, xp_reward, rule_key} objects.';



CREATE OR REPLACE VIEW "public"."vw_xp_leaderboard_season" AS
 WITH "ranked" AS (
         SELECT "st"."season_id",
            "s"."slug" AS "season_slug",
            "st"."app_id",
            "st"."lenser_id",
            "st"."total_xp",
            "rank"() OVER (PARTITION BY "st"."season_id", "st"."app_id" ORDER BY "st"."total_xp" DESC, "st"."lenser_id") AS "rank"
           FROM ("xp"."season_totals" "st"
             JOIN "xp"."seasons" "s" ON (("s"."id" = "st"."season_id")))
        ), "top100" AS (
         SELECT "ranked"."season_id",
            "ranked"."season_slug",
            "ranked"."app_id",
            "ranked"."lenser_id",
            "ranked"."total_xp",
            "ranked"."rank"
           FROM "ranked"
          WHERE ("ranked"."rank" <= 100)
        ), "me" AS (
         SELECT "ranked"."season_id",
            "ranked"."season_slug",
            "ranked"."app_id",
            "ranked"."lenser_id",
            "ranked"."total_xp",
            "ranked"."rank"
           FROM "ranked"
          WHERE ("ranked"."lenser_id" = "auth"."uid"())
        )
 SELECT "t"."season_id",
    "t"."season_slug",
    "t"."app_id",
    "t"."rank",
    "t"."lenser_id",
    "t"."total_xp",
    "jsonb_build_object"('display_name', "l"."display_name", 'handle', "l"."handle", 'avatar_url', "l"."avatar_url") AS "user"
   FROM ("top100" "t"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "t"."lenser_id")))
UNION
 SELECT "m"."season_id",
    "m"."season_slug",
    "m"."app_id",
    "m"."rank",
    "m"."lenser_id",
    "m"."total_xp",
    "jsonb_build_object"('display_name', "l"."display_name", 'handle', "l"."handle", 'avatar_url', "l"."avatar_url") AS "user"
   FROM ("me" "m"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "m"."lenser_id")));


ALTER VIEW "public"."vw_xp_leaderboard_season" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."workflow_integration_credentials" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "integration_type" "text" NOT NULL,
    "label" "text",
    "encrypted_config" "bytea" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_wic_label_length" CHECK ((("label" IS NULL) OR ("length"("label") <= 100))),
    CONSTRAINT "chk_wic_type_valid" CHECK (("integration_type" = ANY (ARRAY['email'::"text", 'slack'::"text", 'discord'::"text", 'github'::"text", 'notion'::"text", 'google_sheets'::"text"])))
);


ALTER TABLE "public"."workflow_integration_credentials" OWNER TO "postgres";


COMMENT ON TABLE "public"."workflow_integration_credentials" IS 'AES-GCM encrypted credentials for third-party integrations. Decrypt only via SECURITY DEFINER fn.';



CREATE TABLE IF NOT EXISTS "public"."workflow_kv_store" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "run_id" "uuid",
    "key" "text" NOT NULL,
    "value" "jsonb",
    "expires_at" timestamp with time zone DEFAULT ("now"() + '24:00:00'::interval) NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_kv_key_format" CHECK (("key" ~ '^[\w\-\.]+$'::"text")),
    CONSTRAINT "chk_kv_key_length" CHECK (("length"("key") <= 256))
);


ALTER TABLE "public"."workflow_kv_store" OWNER TO "postgres";


COMMENT ON TABLE "public"."workflow_kv_store" IS 'Ephemeral key-value state for workflow runs. Scoped by run_id. TTL enforced by pg_cron.';



CREATE TABLE IF NOT EXISTS "public"."workflow_webhook_triggers" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "secret" "text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_webhook_secret_length" CHECK ((("length"("secret") >= 16) AND ("length"("secret") <= 512)))
);


ALTER TABLE "public"."workflow_webhook_triggers" OWNER TO "postgres";


COMMENT ON TABLE "public"."workflow_webhook_triggers" IS 'Inbound webhook endpoints that trigger workflow runs on POST.';



CREATE TABLE IF NOT EXISTS "reputation"."elo_battle_log" (
    "battle_id" "uuid" NOT NULL,
    "computed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "winner_lenser_id" "uuid",
    "loser_lenser_id" "uuid",
    "is_draw" boolean DEFAULT false NOT NULL,
    "winner_score_before" numeric(10,4),
    "winner_score_after" numeric(10,4),
    "loser_score_before" numeric(10,4),
    "loser_score_after" numeric(10,4),
    "k_factor" integer DEFAULT 32 NOT NULL
);

ALTER TABLE ONLY "reputation"."elo_battle_log" FORCE ROW LEVEL SECURITY;


ALTER TABLE "reputation"."elo_battle_log" OWNER TO "postgres";


COMMENT ON TABLE "reputation"."elo_battle_log" IS 'Phase O3: idempotency + audit log for per-battle ELO updates. PK on battle_id ensures each battle is scored at most once.';



CREATE TABLE IF NOT EXISTS "reputation"."judge_calibrations" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "calibration_score" numeric(5,4) DEFAULT 0.5 NOT NULL,
    "total_judgments" integer DEFAULT 0 NOT NULL,
    "agreement_rate" numeric(5,4) DEFAULT 0.5 NOT NULL,
    "kappa_score" numeric(5,4),
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "agreement_rate_range" CHECK ((("agreement_rate" >= (0)::numeric) AND ("agreement_rate" <= (1)::numeric))),
    CONSTRAINT "calibration_judgments_nonneg" CHECK (("total_judgments" >= 0)),
    CONSTRAINT "calibration_score_range" CHECK ((("calibration_score" >= (0)::numeric) AND ("calibration_score" <= (1)::numeric))),
    CONSTRAINT "ck_reputation_judge_calibrations_calibration_score_nonneg" CHECK ((("calibration_score" IS NULL) OR ("calibration_score" >= (0)::numeric))),
    CONSTRAINT "ck_reputation_judge_calibrations_kappa_score_nonneg" CHECK ((("kappa_score" IS NULL) OR ("kappa_score" >= (0)::numeric))),
    CONSTRAINT "ck_reputation_judge_calibrations_total_judgments_nonneg" CHECK ((("total_judgments" IS NULL) OR ("total_judgments" >= 0))),
    CONSTRAINT "kappa_range" CHECK ((("kappa_score" IS NULL) OR (("kappa_score" >= ('-1'::integer)::numeric) AND ("kappa_score" <= (1)::numeric))))
);


ALTER TABLE "reputation"."judge_calibrations" OWNER TO "postgres";


COMMENT ON TABLE "reputation"."judge_calibrations" IS 'Vote quality metrics for each judge (human or AI). calibration_score: overall judge reliability [0..1]. agreement_rate: fraction of votes agreeing with eventual battle consensus. kappa_score: Cohen''s kappa against expert panel (NULL until calibrated). Used to weight votes in trust-adjusted scoring.';



CREATE OR REPLACE VIEW "reputation"."v_trueskill_leaderboard" AS
 SELECT "cr"."lenser_id",
    "cr"."category",
    ("cr"."elo_rating" - (3.0 * "cr"."uncertainty")) AS "conservative_rating",
    "cr"."elo_rating" AS "mu",
    "cr"."uncertainty" AS "sigma",
    "cr"."tau",
    "cr"."beta",
    "cr"."battles_played",
    "cr"."wins",
    "cr"."draws",
    "cr"."losses",
    "cr"."updated_at",
    "p"."handle",
    "p"."display_name",
    "p"."avatar_url",
    "p"."type" AS "lenser_type",
    "dense_rank"() OVER (PARTITION BY "cr"."category" ORDER BY ("cr"."elo_rating" - (3.0 * "cr"."uncertainty")) DESC) AS "rank"
   FROM ("reputation"."contender_ratings" "cr"
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "cr"."lenser_id")))
  WHERE (("cr"."battles_played" >= 3) AND ("p"."status" = 'active'::"lensers"."lenser_status"))
  ORDER BY ("cr"."elo_rating" - (3.0 * "cr"."uncertainty")) DESC;


ALTER VIEW "reputation"."v_trueskill_leaderboard" OWNER TO "postgres";


COMMENT ON VIEW "reputation"."v_trueskill_leaderboard" IS 'TrueSkill conservative rating leaderboard. Rating = mu - 3*sigma. Min 3 battles to appear.';



CREATE TABLE IF NOT EXISTS "reputation"."vote_risk_scores" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "vote_id" "uuid" NOT NULL,
    "risk_score" numeric(5,4) DEFAULT 0 NOT NULL,
    "risk_factors" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "review_status" "text" DEFAULT 'cleared'::"text" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_reputation_vote_risk_scores_risk_score_nonneg" CHECK ((("risk_score" IS NULL) OR ("risk_score" >= (0)::numeric))),
    CONSTRAINT "risk_review_status_check" CHECK (("review_status" = ANY (ARRAY['cleared'::"text", 'flagged'::"text", 'excluded'::"text", 'appealed'::"text"]))),
    CONSTRAINT "risk_score_range" CHECK ((("risk_score" >= (0)::numeric) AND ("risk_score" <= (1)::numeric)))
);


ALTER TABLE "reputation"."vote_risk_scores" OWNER TO "postgres";


COMMENT ON TABLE "reputation"."vote_risk_scores" IS 'Fraud risk assessment per vote. Computed asynchronously after vote insert. risk_score=0 = low risk, 1 = high risk. review_status=excluded means vote is removed from aggregation. Future: split into vote_risk_scores (computation) + vote_reviews (workflow).';



CREATE TABLE IF NOT EXISTS "status"."components" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "service_id" "uuid" NOT NULL,
    "slug" "text" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "current_status" "status"."status_state" DEFAULT 'operational'::"status"."status_state" NOT NULL,
    "indicator" "text" DEFAULT 'none'::"text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "sort_order" integer DEFAULT 0 NOT NULL,
    "is_public" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);

ALTER TABLE ONLY "status"."components" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."components" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "status"."daily_snapshots" (
    "snapshot_date" "date" NOT NULL,
    "scope_type" "text" NOT NULL,
    "scope_key" "text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "computed_at" timestamp with time zone DEFAULT "now"() NOT NULL
);

ALTER TABLE ONLY "status"."daily_snapshots" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."daily_snapshots" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "status"."incident_updates" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "incident_id" "uuid" NOT NULL,
    "status" "status"."incident_stage" NOT NULL,
    "message" "text" NOT NULL,
    "author_name" "text",
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);

ALTER TABLE ONLY "status"."incident_updates" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."incident_updates" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "status"."incidents" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "title" "text" NOT NULL,
    "status" "status"."incident_stage" DEFAULT 'investigating'::"status"."incident_stage" NOT NULL,
    "impact" "status"."incident_impact" DEFAULT 'minor'::"status"."incident_impact" NOT NULL,
    "severity" "text",
    "description" "text",
    "service_id" "uuid",
    "published" boolean DEFAULT true NOT NULL,
    "started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "resolved_at" timestamp with time zone,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);

ALTER TABLE ONLY "status"."incidents" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."incidents" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "status"."maintenance_windows" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "title" "text" NOT NULL,
    "status" "status"."maintenance_status" DEFAULT 'scheduled'::"status"."maintenance_status" NOT NULL,
    "service_id" "uuid",
    "starts_at" timestamp with time zone NOT NULL,
    "ends_at" timestamp with time zone NOT NULL,
    "published" boolean DEFAULT true NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "maintenance_windows_valid_range" CHECK (("ends_at" > "starts_at"))
);

ALTER TABLE ONLY "status"."maintenance_windows" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."maintenance_windows" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "status"."services" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "current_status" "status"."status_state" DEFAULT 'operational'::"status"."status_state" NOT NULL,
    "indicator" "text" DEFAULT 'none'::"text" NOT NULL,
    "payload" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "sort_order" integer DEFAULT 0 NOT NULL,
    "is_public" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);

ALTER TABLE ONLY "status"."services" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."services" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "status"."uptime_rollups" (
    "scope_type" "text" NOT NULL,
    "scope_key" "text" NOT NULL,
    "rollup_window" "status"."status_window" NOT NULL,
    "uptime_percent" numeric(6,3) DEFAULT 100 NOT NULL,
    "downtime_minutes" numeric(10,2) DEFAULT 0 NOT NULL,
    "incident_count" integer DEFAULT 0 NOT NULL,
    "computed_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_status_uptime_rollups_incident_count_nonneg" CHECK ((("incident_count" IS NULL) OR ("incident_count" >= 0))),
    CONSTRAINT "downtime_minutes_nonnegative" CHECK (("downtime_minutes" >= (0)::numeric)),
    CONSTRAINT "incident_count_nonnegative" CHECK (("incident_count" >= 0)),
    CONSTRAINT "uptime_percent_range" CHECK ((("uptime_percent" >= (0)::numeric) AND ("uptime_percent" <= (100)::numeric)))
);

ALTER TABLE ONLY "status"."uptime_rollups" FORCE ROW LEVEL SECURITY;


ALTER TABLE "status"."uptime_rollups" OWNER TO "postgres";


CREATE OR REPLACE VIEW "status"."v_public_components" WITH ("security_invoker"='true') AS
 SELECT "c"."id",
    "c"."slug",
    "c"."name",
    "c"."description",
    "c"."current_status",
    "c"."indicator",
    "c"."service_id",
    "c"."payload",
    "c"."sort_order",
    "c"."is_public",
    "c"."created_at",
    "c"."updated_at"
   FROM ("status"."components" "c"
     JOIN "status"."services" "s" ON (("s"."id" = "c"."service_id")))
  WHERE (("c"."is_public" = true) AND ("s"."is_public" = true));


ALTER VIEW "status"."v_public_components" OWNER TO "postgres";


CREATE OR REPLACE VIEW "status"."v_public_incident_updates" WITH ("security_invoker"='true') AS
 SELECT "u"."id",
    "u"."incident_id",
    "u"."status",
    "u"."message",
    "u"."author_name",
    "u"."payload",
    "u"."created_at"
   FROM (("status"."incident_updates" "u"
     JOIN "status"."incidents" "i" ON (("i"."id" = "u"."incident_id")))
     LEFT JOIN "status"."services" "s" ON (("s"."id" = "i"."service_id")))
  WHERE (("i"."published" = true) AND (("i"."service_id" IS NULL) OR ("s"."is_public" = true)));


ALTER VIEW "status"."v_public_incident_updates" OWNER TO "postgres";


CREATE OR REPLACE VIEW "status"."v_public_incidents" WITH ("security_invoker"='true') AS
 SELECT "i"."id",
    "i"."slug",
    "i"."title",
    "i"."status",
    "i"."impact",
    "i"."severity",
    "i"."description",
    "i"."service_id",
    "i"."published",
    "i"."started_at",
    "i"."resolved_at",
    "i"."payload",
    "i"."created_at",
    "i"."updated_at"
   FROM ("status"."incidents" "i"
     LEFT JOIN "status"."services" "s" ON (("s"."id" = "i"."service_id")))
  WHERE (("i"."published" = true) AND (("i"."service_id" IS NULL) OR ("s"."is_public" = true)));


ALTER VIEW "status"."v_public_incidents" OWNER TO "postgres";


CREATE OR REPLACE VIEW "status"."v_public_maintenance_windows" WITH ("security_invoker"='true') AS
 SELECT "m"."id",
    "m"."slug",
    "m"."title",
    "m"."status",
    "m"."service_id",
    "m"."starts_at",
    "m"."ends_at",
    "m"."published",
    "m"."payload",
    "m"."created_at",
    "m"."updated_at"
   FROM ("status"."maintenance_windows" "m"
     LEFT JOIN "status"."services" "s" ON (("s"."id" = "m"."service_id")))
  WHERE (("m"."published" = true) AND (("m"."service_id" IS NULL) OR ("s"."is_public" = true)));


ALTER VIEW "status"."v_public_maintenance_windows" OWNER TO "postgres";


CREATE OR REPLACE VIEW "status"."v_public_services" WITH ("security_invoker"='true') AS
 SELECT "id",
    "slug",
    "name",
    "description",
    "current_status",
    "indicator",
    "payload",
    "sort_order",
    "is_public",
    "created_at",
    "updated_at",
    COALESCE(( SELECT "jsonb_object_agg"(("r"."rollup_window")::"text", "r"."uptime_percent" ORDER BY "r"."rollup_window") AS "jsonb_object_agg"
           FROM "status"."uptime_rollups" "r"
          WHERE (("r"."scope_type" = 'service'::"text") AND ("r"."scope_key" = "s"."slug"))), '{}'::"jsonb") AS "uptime"
   FROM "status"."services" "s"
  WHERE ("is_public" = true);


ALTER VIEW "status"."v_public_services" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "tenancy"."workspace_members" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "role" "text" DEFAULT 'member'::"text" NOT NULL,
    "invited_by" "uuid",
    "joined_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workspace_members_role_check" CHECK (("role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'member'::"text", 'viewer'::"text"])))
);


ALTER TABLE "tenancy"."workspace_members" OWNER TO "postgres";


COMMENT ON TABLE "tenancy"."workspace_members" IS 'Workspace membership with roles. Owner has full control, admin can manage members, member has read/write, viewer has read-only.';



CREATE TABLE IF NOT EXISTS "tenancy"."workspaces" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "type" "text" NOT NULL,
    "display_name" "text" NOT NULL,
    "owner_lenser_id" "uuid",
    "org_id" "uuid",
    "status" "text" DEFAULT 'active'::"text" NOT NULL,
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "workspaces_slug_format" CHECK ((("slug" ~ '^[a-z0-9]+([_-][a-z0-9]+)*$'::"text") AND (("char_length"("slug") >= 3) AND ("char_length"("slug") <= 64)))),
    CONSTRAINT "workspaces_status_check" CHECK (("status" = ANY (ARRAY['active'::"text", 'suspended'::"text", 'archived'::"text"]))),
    CONSTRAINT "workspaces_type_check" CHECK (("type" = ANY (ARRAY['personal'::"text", 'organization'::"text"])))
);


ALTER TABLE "tenancy"."workspaces" OWNER TO "postgres";


COMMENT ON TABLE "tenancy"."workspaces" IS 'Canonical tenant boundary. Every lenser gets a personal workspace; organizations map to organization-type workspaces. Media, keys, executions, and audit records are workspace-scoped.';



CREATE TABLE IF NOT EXISTS "wallet"."accounts" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "balance" bigint DEFAULT 0 NOT NULL,
    "lifetime_deposits" bigint DEFAULT 0 NOT NULL,
    "lifetime_spent" bigint DEFAULT 0 NOT NULL,
    "frozen" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "accounts_balance_non_negative" CHECK (("balance" >= 0))
);


ALTER TABLE "wallet"."accounts" OWNER TO "postgres";


COMMENT ON TABLE "wallet"."accounts" IS 'Wallet account per lenser. balance >= 0 enforced at DB level. ON DELETE RESTRICT — close wallet before deleting profile.';



COMMENT ON COLUMN "wallet"."accounts"."balance" IS 'Current credit balance. Always >= 0. Updated atomically by wallet functions.';



COMMENT ON COLUMN "wallet"."accounts"."lifetime_deposits" IS 'Cumulative credits deposited (monotonically increasing).';



COMMENT ON COLUMN "wallet"."accounts"."lifetime_spent" IS 'Cumulative credits spent (monotonically increasing).';



COMMENT ON COLUMN "wallet"."accounts"."frozen" IS 'Admin suspension flag. Frozen accounts cannot transact.';



CREATE TABLE IF NOT EXISTS "wallet"."credit_valuation_policy" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "singleton" boolean DEFAULT true NOT NULL,
    "baseline_credits_per_usd" numeric(12,4) NOT NULL,
    "tier_small_step" numeric(12,6),
    "tier_large_threshold" numeric(12,6),
    "tier_large_step" numeric(12,6),
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "credit_valuation_rate_positive" CHECK (("baseline_credits_per_usd" > (0)::numeric)),
    CONSTRAINT "credit_valuation_singleton_true" CHECK (("singleton" = true))
);


ALTER TABLE "wallet"."credit_valuation_policy" OWNER TO "postgres";


COMMENT ON TABLE "wallet"."credit_valuation_policy" IS 'Singleton table holding the global credit-to-USD conversion rate. 1 credit = 1 / baseline_credits_per_usd USD. With baseline_credits_per_usd = 100, 1 credit = $0.01.';



CREATE TABLE IF NOT EXISTS "wallet"."organization_accounts" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "org_id" "uuid" NOT NULL,
    "balance" bigint DEFAULT 0 NOT NULL,
    "lifetime_deposits" bigint DEFAULT 0 NOT NULL,
    "lifetime_spent" bigint DEFAULT 0 NOT NULL,
    "frozen" boolean DEFAULT false NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "org_accounts_balance_non_negative" CHECK (("balance" >= 0))
);


ALTER TABLE "wallet"."organization_accounts" OWNER TO "postgres";


COMMENT ON TABLE "wallet"."organization_accounts" IS 'Wallet account per organization (team/community). Mirrors wallet.accounts structure. Functions deferred — table created for schema readiness.';



COMMENT ON COLUMN "wallet"."organization_accounts"."org_id" IS 'FK to actors.organizations. ON DELETE RESTRICT — close wallet before deleting org.';



CREATE TABLE IF NOT EXISTS "wallet"."pending_charges" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "account_id" "uuid" NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "reserved_amount" bigint NOT NULL,
    "settled_amount" bigint,
    "status" "wallet"."charge_status_enum" DEFAULT 'reserved'::"wallet"."charge_status_enum" NOT NULL,
    "reference_id" "uuid" NOT NULL,
    "description" "text",
    "reserved_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "settled_at" timestamp with time zone,
    CONSTRAINT "pending_charges_reserved_positive" CHECK (("reserved_amount" > 0)),
    CONSTRAINT "pending_charges_settled_non_negative" CHECK ((("settled_amount" IS NULL) OR ("settled_amount" >= 0)))
);


ALTER TABLE "wallet"."pending_charges" OWNER TO "postgres";


COMMENT ON TABLE "wallet"."pending_charges" IS 'Pre-authorization ledger for execution runs. Tracks reserved → settled/released lifecycle. Enforces idempotency via reference_id UNIQUE constraint.';



COMMENT ON COLUMN "wallet"."pending_charges"."reserved_amount" IS 'Credits debited from account when reservation created. Remains until charge settled or released.';



COMMENT ON COLUMN "wallet"."pending_charges"."settled_amount" IS 'Actual credits charged after execution completes. NULL until settled. If NULL at release, full refund applied.';



COMMENT ON COLUMN "wallet"."pending_charges"."reference_id" IS 'FK to execution.runs.id. UNIQUE constraint prevents double-charging on Worker retry.';



CREATE TABLE IF NOT EXISTS "wallet"."spending_limits" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "account_id" "uuid" NOT NULL,
    "period" "text" NOT NULL,
    "limit_credits" bigint NOT NULL,
    "current_usage" bigint DEFAULT 0 NOT NULL,
    "period_start" timestamp with time zone DEFAULT "now"() NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "spending_limits_limit_check" CHECK (("limit_credits" > 0)),
    CONSTRAINT "spending_limits_period_check" CHECK (("period" = ANY (ARRAY['daily'::"text", 'monthly'::"text"])))
);


ALTER TABLE "wallet"."spending_limits" OWNER TO "postgres";


COMMENT ON TABLE "wallet"."spending_limits" IS 'Optional per-account spending caps. current_usage reset to 0 by pg_cron at period boundaries.';



CREATE TABLE IF NOT EXISTS "wallet"."transactions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "account_id" "uuid" NOT NULL,
    "tx_type" "wallet"."transaction_type_enum" NOT NULL,
    "amount" bigint NOT NULL,
    "direction" smallint NOT NULL,
    "balance_after" bigint NOT NULL,
    "reference_type" "text",
    "reference_id" "uuid",
    "description" "text",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "idempotency_key" "text",
    CONSTRAINT "transactions_amount_positive" CHECK (("amount" > 0)),
    CONSTRAINT "transactions_direction_check" CHECK (("direction" = ANY (ARRAY[1, '-1'::integer])))
);


ALTER TABLE "wallet"."transactions" OWNER TO "postgres";


COMMENT ON TABLE "wallet"."transactions" IS 'Append-only wallet transaction ledger. No UPDATE/DELETE allowed. balance_after enables audit verification: balance_after[n] = balance_after[n-1] + (amount * direction).';



COMMENT ON COLUMN "wallet"."transactions"."amount" IS 'Always positive. Effective amount = amount * direction.';



COMMENT ON COLUMN "wallet"."transactions"."direction" IS '+1 for credits (deposit, refund, sponsorship_payout), -1 for debits (spend, sponsorship_deposit, platform_fee).';



COMMENT ON COLUMN "wallet"."transactions"."balance_after" IS 'Account balance after this transaction. Enables full audit trail verification.';



COMMENT ON COLUMN "wallet"."transactions"."reference_type" IS 'Source entity type, e.g. billing.orders, execution.runs, battles.sponsorship_contributions.';



COMMENT ON COLUMN "wallet"."transactions"."reference_id" IS 'Source entity UUID. Combined with reference_type for cross-schema tracing.';



COMMENT ON COLUMN "wallet"."transactions"."idempotency_key" IS 'Optional idempotency key for dedup. When set, prevents duplicate transactions from retried API calls. SHA-256 hash of scope|actor|operation context.';



CREATE TABLE IF NOT EXISTS "xp"."apps" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "slug" "text" NOT NULL,
    "name" "text" NOT NULL,
    "difficulty" "xp"."difficulty_enum" DEFAULT 'standard'::"xp"."difficulty_enum" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "metadata" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "xp"."apps" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."contributions" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "context" "xp"."contribution_context_enum" NOT NULL,
    "contribution_type" "text" NOT NULL,
    "external_ref" "text",
    "title" "text",
    "verified_by" "text",
    "xp_event_id" "uuid",
    "metadata" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "xp"."contributions" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."difficulty_multipliers" (
    "difficulty" "xp"."difficulty_enum" NOT NULL,
    "multiplier" numeric(4,2) NOT NULL,
    "label" "text" NOT NULL
);


ALTER TABLE "xp"."difficulty_multipliers" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."event_verifications" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "event_id" "uuid" NOT NULL,
    "ai_verified" boolean NOT NULL,
    "verifier" "text",
    "details" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "xp"."event_verifications" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."events" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid",
    "app_id" "uuid" NOT NULL,
    "rule_id" "uuid" NOT NULL,
    "action_key" "text" NOT NULL,
    "xp" integer NOT NULL,
    "base_xp" integer NOT NULL,
    "source" "xp"."source_enum" NOT NULL,
    "source_ref_type" "text",
    "source_ref_id" "uuid",
    "signature" "text",
    "ai_verified" boolean DEFAULT false,
    "meta" "jsonb",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "season_id" "uuid",
    CONSTRAINT "ck_xp_events_xp_nonneg" CHECK ((("xp" IS NULL) OR ("xp" >= 0)))
);


ALTER TABLE "xp"."events" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."level_ups" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "app_id" "uuid" NOT NULL,
    "old_level" integer NOT NULL,
    "new_level" integer NOT NULL,
    "total_xp_at" bigint NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_xp_level_ups_total_xp_at_nonneg" CHECK ((("total_xp_at" IS NULL) OR ("total_xp_at" >= 0)))
);


ALTER TABLE "xp"."level_ups" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."monthly_rollup" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "year" integer NOT NULL,
    "month" integer NOT NULL,
    "xp" bigint DEFAULT 0 NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "ck_xp_monthly_rollup_xp_nonneg" CHECK ((("xp" IS NULL) OR ("xp" >= 0)))
);


ALTER TABLE "xp"."monthly_rollup" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."policy" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "policy" "jsonb" NOT NULL,
    "signature" "text",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "xp"."policy" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."rules" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "app_id" "uuid" NOT NULL,
    "action_key" "text" NOT NULL,
    "name" "text" NOT NULL,
    "description" "text",
    "base_xp" integer NOT NULL,
    "max_xp_per_day" integer,
    "max_events_per_day" integer,
    "cooldown_seconds" integer,
    "max_xp_per_season" integer,
    "streak_type" "text",
    "signature_required" boolean DEFAULT false NOT NULL,
    "metadata" "jsonb",
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "difficulty" "xp"."difficulty_enum" DEFAULT 'standard'::"xp"."difficulty_enum"
);


ALTER TABLE "xp"."rules" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "xp"."streaks" (
    "lenser_id" "uuid" NOT NULL,
    "app_id" "uuid" NOT NULL,
    "streak_type" "text" NOT NULL,
    "current_streak" integer DEFAULT 0 NOT NULL,
    "longest_streak" integer DEFAULT 0 NOT NULL,
    "last_occurrence_at" "date",
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "xp"."streaks" OWNER TO "postgres";


CREATE SEQUENCE IF NOT EXISTS "xp"."xp_levels_id_seq"
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER SEQUENCE "xp"."xp_levels_id_seq" OWNER TO "postgres";


ALTER SEQUENCE "xp"."xp_levels_id_seq" OWNED BY "xp"."levels"."id";



ALTER TABLE ONLY "analytics"."lenser_join_log" ALTER COLUMN "join_order" SET DEFAULT "nextval"('"analytics"."lenser_join_sequence"'::"regclass");



ALTER TABLE ONLY "analytics"."tag_activity_events" ALTER COLUMN "id" SET DEFAULT "nextval"('"analytics"."tag_activity_events_id_seq"'::"regclass");



ALTER TABLE ONLY "audit"."dedupe_log" ALTER COLUMN "id" SET DEFAULT "nextval"('"audit"."dedupe_log_id_seq"'::"regclass");



ALTER TABLE ONLY "automation"."cron_runs" ALTER COLUMN "id" SET DEFAULT "nextval"('"automation"."cron_runs_id_seq"'::"regclass");



ALTER TABLE ONLY "billing"."ledger_entries" ALTER COLUMN "id" SET DEFAULT "nextval"('"billing"."ledger_entries_id_seq"'::"regclass");



ALTER TABLE ONLY "core"."cities" ALTER COLUMN "id" SET DEFAULT "nextval"('"core"."cities_id_seq"'::"regclass");



ALTER TABLE ONLY "core"."countries" ALTER COLUMN "id" SET DEFAULT "nextval"('"core"."countries_id_seq"'::"regclass");



ALTER TABLE ONLY "core"."regions" ALTER COLUMN "id" SET DEFAULT "nextval"('"core"."regions_id_seq"'::"regclass");



ALTER TABLE ONLY "core"."states" ALTER COLUMN "id" SET DEFAULT "nextval"('"core"."states_id_seq"'::"regclass");



ALTER TABLE ONLY "core"."subregions" ALTER COLUMN "id" SET DEFAULT "nextval"('"core"."subregions_id_seq"'::"regclass");



ALTER TABLE ONLY "xp"."levels" ALTER COLUMN "id" SET DEFAULT "nextval"('"xp"."xp_levels_id_seq"'::"regclass");



ALTER TABLE ONLY "admin"."execution_control"
    ADD CONSTRAINT "execution_control_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "admin"."kill_switches"
    ADD CONSTRAINT "kill_switches_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."action_logs"
    ADD CONSTRAINT "action_logs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."agent_run_events"
    ADD CONSTRAINT "agent_run_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."agent_run_steps"
    ADD CONSTRAINT "agent_run_steps_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."ai_lensers"
    ADD CONSTRAINT "ai_lensers_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."ai_lensers"
    ADD CONSTRAINT "ai_lensers_profile_id_key" UNIQUE ("profile_id");



ALTER TABLE ONLY "agents"."battle_subscriptions"
    ADD CONSTRAINT "battle_subscriptions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."evaluation_baselines"
    ADD CONSTRAINT "evaluation_baselines_one_per_eval" UNIQUE ("evaluation_id");



ALTER TABLE ONLY "agents"."evaluation_baselines"
    ADD CONSTRAINT "evaluation_baselines_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."evaluation_case_results"
    ADD CONSTRAINT "evaluation_case_results_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."evaluation_cases"
    ADD CONSTRAINT "evaluation_cases_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."evaluation_rubrics"
    ADD CONSTRAINT "evaluation_rubrics_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."evaluation_runs"
    ADD CONSTRAINT "evaluation_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."evaluations"
    ADD CONSTRAINT "evaluations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."gateway_commands"
    ADD CONSTRAINT "gateway_commands_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."gateway_devices"
    ADD CONSTRAINT "gateway_devices_pkey" PRIMARY KEY ("device_id");



ALTER TABLE ONLY "agents"."lens_bindings"
    ADD CONSTRAINT "lens_bindings_ai_lenser_id_lens_id_key" UNIQUE ("ai_lenser_id", "lens_id");



ALTER TABLE ONLY "agents"."lens_bindings"
    ADD CONSTRAINT "lens_bindings_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."memories"
    ADD CONSTRAINT "memories_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."memory_access_logs"
    ADD CONSTRAINT "memory_access_logs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."memory_profiles"
    ADD CONSTRAINT "memory_profiles_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."model_bindings"
    ADD CONSTRAINT "model_bindings_ai_lenser_id_model_id_key" UNIQUE ("ai_lenser_id", "model_id");



ALTER TABLE ONLY "agents"."model_bindings"
    ADD CONSTRAINT "model_bindings_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."model_profiles"
    ADD CONSTRAINT "model_profiles_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."ownerships"
    ADD CONSTRAINT "ownerships_ai_lenser_id_owner_lenser_id_key" UNIQUE ("ai_lenser_id", "owner_lenser_id");



ALTER TABLE ONLY "agents"."ownerships"
    ADD CONSTRAINT "ownerships_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."personality_profiles"
    ADD CONSTRAINT "personality_profiles_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."policies"
    ADD CONSTRAINT "policies_ai_lenser_id_key" UNIQUE ("ai_lenser_id");



ALTER TABLE ONLY "agents"."policies"
    ADD CONSTRAINT "policies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."policy_evaluations"
    ADD CONSTRAINT "policy_evaluations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."provider_configs"
    ADD CONSTRAINT "provider_configs_ai_lenser_id_provider_key_key" UNIQUE ("ai_lenser_id", "provider_key");



ALTER TABLE ONLY "agents"."provider_configs"
    ADD CONSTRAINT "provider_configs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."quota_snapshots"
    ADD CONSTRAINT "quota_snapshots_ai_lenser_id_period_date_key" UNIQUE ("ai_lenser_id", "period_date");



ALTER TABLE ONLY "agents"."quota_snapshots"
    ADD CONSTRAINT "quota_snapshots_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."run_incidents"
    ADD CONSTRAINT "run_incidents_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."run_reports"
    ADD CONSTRAINT "run_reports_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."scratchpad_runs"
    ADD CONSTRAINT "scratchpad_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."standing_approvals"
    ADD CONSTRAINT "standing_approvals_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."team_edges"
    ADD CONSTRAINT "team_edges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_unique_agent" UNIQUE ("team_id", "agent_id");



ALTER TABLE ONLY "agents"."team_messages"
    ADD CONSTRAINT "team_messages_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."teams"
    ADD CONSTRAINT "teams_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."tool_assignments"
    ADD CONSTRAINT "tool_assignments_ai_lenser_id_tool_id_key" UNIQUE ("ai_lenser_id", "tool_id");



ALTER TABLE ONLY "agents"."tool_assignments"
    ADD CONSTRAINT "tool_assignments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."tool_invocations"
    ADD CONSTRAINT "tool_invocations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."tool_profiles"
    ADD CONSTRAINT "tool_profiles_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."tools_registry"
    ADD CONSTRAINT "tools_registry_owner_lenser_id_key_key" UNIQUE ("owner_lenser_id", "key");



ALTER TABLE ONLY "agents"."tools_registry"
    ADD CONSTRAINT "tools_registry_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."workflow_assignments"
    ADD CONSTRAINT "workflow_assignments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "agents"."workspace_settings"
    ADD CONSTRAINT "workspace_settings_pkey" PRIMARY KEY ("ai_lenser_id");



ALTER TABLE ONLY "agents"."workspace_switches"
    ADD CONSTRAINT "workspace_switches_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "ai"."models"
    ADD CONSTRAINT "ai_models_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "ai"."key_usage_log"
    ADD CONSTRAINT "key_usage_log_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "ai"."keys"
    ADD CONSTRAINT "keys_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "ai"."modality_pricing"
    ADD CONSTRAINT "modality_pricing_model_id_output_modality_key" UNIQUE ("model_id", "output_modality");



ALTER TABLE ONLY "ai"."modality_pricing"
    ADD CONSTRAINT "modality_pricing_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "ai"."model_pricing"
    ADD CONSTRAINT "model_pricing_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "ai"."models"
    ADD CONSTRAINT "models_key_unique" UNIQUE ("key");



ALTER TABLE ONLY "ai"."providers"
    ADD CONSTRAINT "providers_key_unique" UNIQUE ("key");



ALTER TABLE ONLY "ai"."providers"
    ADD CONSTRAINT "providers_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."agent_cost_daily"
    ADD CONSTRAINT "acd_lenser_model_date" UNIQUE ("ai_lenser_id", "model_key", "period_date");



ALTER TABLE ONLY "analytics"."agent_cost_daily"
    ADD CONSTRAINT "agent_cost_daily_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."contact"
    ADD CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."eval_quality_daily"
    ADD CONSTRAINT "eqd_lenser_eval_date" UNIQUE ("ai_lenser_id", "evaluation_id", "period_date");



ALTER TABLE ONLY "analytics"."eval_quality_daily"
    ADD CONSTRAINT "eval_quality_daily_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."product_feedback"
    ADD CONSTRAINT "feedback_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."lenser_activity"
    ADD CONSTRAINT "lenser_activity_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."lenser_stats"
    ADD CONSTRAINT "lenser_engagement_totals_pkey" PRIMARY KEY ("lenser_id");



ALTER TABLE ONLY "analytics"."lenser_join_log"
    ADD CONSTRAINT "lenser_join_log_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."lenser_join_log"
    ADD CONSTRAINT "lenser_join_log_unique_lenser" UNIQUE ("lenser_id");



ALTER TABLE ONLY "analytics"."page_views"
    ADD CONSTRAINT "page_views_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."share_events"
    ADD CONSTRAINT "share_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."shared_links"
    ADD CONSTRAINT "shared_links_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."shared_links"
    ADD CONSTRAINT "shared_links_short_id_key" UNIQUE ("short_id");



ALTER TABLE ONLY "analytics"."tag_activity_events"
    ADD CONSTRAINT "tag_activity_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."workflow_perf_daily"
    ADD CONSTRAINT "workflow_perf_daily_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "analytics"."workflow_perf_daily"
    ADD CONSTRAINT "wpd_lenser_workflow_date" UNIQUE ("ai_lenser_id", "workflow_id", "period_date");



ALTER TABLE ONLY "audit"."account_lifecycle"
    ADD CONSTRAINT "account_lifecycle_audit_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."admin_actions"
    ADD CONSTRAINT "admin_actions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."attestations"
    ADD CONSTRAINT "attestations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."byok_key_usage"
    ADD CONSTRAINT "byok_key_usage_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."dedupe_log"
    ADD CONSTRAINT "dedupe_log_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."disputes"
    ADD CONSTRAINT "disputes_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."events"
    ADD CONSTRAINT "events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."hash_chains"
    ADD CONSTRAINT "hash_chains_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."idempotency_keys"
    ADD CONSTRAINT "idempotency_registry_pkey" PRIMARY KEY ("scope", "idempotency_key");



ALTER TABLE ONLY "audit"."idempotency_requests"
    ADD CONSTRAINT "idempotency_requests_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."moderation_decisions"
    ADD CONSTRAINT "moderation_decisions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."security_events"
    ADD CONSTRAINT "security_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."vote_anomalies"
    ADD CONSTRAINT "vote_anomalies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "audit"."webhook_outbox"
    ADD CONSTRAINT "webhook_outbox_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "authz"."developer_tokens"
    ADD CONSTRAINT "developer_tokens_issued_from_request_id_key" UNIQUE ("issued_from_request_id");



ALTER TABLE ONLY "authz"."developer_tokens"
    ADD CONSTRAINT "developer_tokens_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "authz"."developer_tokens"
    ADD CONSTRAINT "developer_tokens_token_hash_key" UNIQUE ("token_hash");



ALTER TABLE ONLY "authz"."device_approval_requests"
    ADD CONSTRAINT "device_approval_requests_developer_token_id_key" UNIQUE ("developer_token_id");



ALTER TABLE ONLY "authz"."device_approval_requests"
    ADD CONSTRAINT "device_approval_requests_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "authz"."device_approval_requests"
    ADD CONSTRAINT "device_approval_requests_request_secret_hash_key" UNIQUE ("request_secret_hash");



ALTER TABLE ONLY "authz"."device_approval_requests"
    ADD CONSTRAINT "device_approval_requests_user_code_key" UNIQUE ("user_code");



ALTER TABLE ONLY "automation"."cron_runs"
    ADD CONSTRAINT "cron_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "automation"."event_dispatches"
    ADD CONSTRAINT "event_dispatches_pkey" PRIMARY KEY ("event_id", "rule_id");



ALTER TABLE ONLY "automation"."events"
    ADD CONSTRAINT "events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "automation"."trigger_rules"
    ADD CONSTRAINT "trigger_rules_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."ai_handicap_policies"
    ADD CONSTRAINT "ai_handicap_one_per_battle" UNIQUE ("battle_id");



ALTER TABLE ONLY "battles"."ai_handicap_policies"
    ADD CONSTRAINT "ai_handicap_policies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."ai_judge_verdicts"
    ADD CONSTRAINT "ai_judge_verdicts_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."battle_event_subscriptions"
    ADD CONSTRAINT "battle_event_subscriptions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."battle_execution_dead_letters"
    ADD CONSTRAINT "battle_execution_dead_letters_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."battle_execution_jobs"
    ADD CONSTRAINT "battle_execution_jobs_battle_id_contender_id_key" UNIQUE ("battle_id", "contender_id");



ALTER TABLE ONLY "battles"."battle_execution_jobs"
    ADD CONSTRAINT "battle_execution_jobs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_invite_code_unique" UNIQUE ("invite_code");



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_slug_unique" UNIQUE ("slug");



ALTER TABLE ONLY "battles"."comments"
    ADD CONSTRAINT "comments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."contender_entity_map"
    ADD CONSTRAINT "contender_entity_map_pkey" PRIMARY KEY ("contender_id");



ALTER TABLE ONLY "battles"."contender_lens_assignments"
    ADD CONSTRAINT "contender_lens_assignments_contender_id_key" UNIQUE ("contender_id");



ALTER TABLE ONLY "battles"."contender_lens_assignments"
    ADD CONSTRAINT "contender_lens_assignments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."contender_runs"
    ADD CONSTRAINT "contender_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."contender_runs"
    ADD CONSTRAINT "contender_runs_unique" UNIQUE ("contender_id", "run_id");



ALTER TABLE ONLY "battles"."contenders"
    ADD CONSTRAINT "contenders_battle_ref_unique" UNIQUE ("battle_id", "contender_ref_id");



ALTER TABLE ONLY "battles"."contenders"
    ADD CONSTRAINT "contenders_battle_slot_unique" UNIQUE ("battle_id", "slot");



ALTER TABLE ONLY "battles"."contenders"
    ADD CONSTRAINT "contenders_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."cost_ledgers"
    ADD CONSTRAINT "cost_ledgers_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."events"
    ADD CONSTRAINT "events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."execution_configs"
    ADD CONSTRAINT "execution_configs_battle_id_contender_id_key" UNIQUE ("battle_id", "contender_id");



ALTER TABLE ONLY "battles"."execution_configs"
    ADD CONSTRAINT "execution_configs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."funding_policies"
    ADD CONSTRAINT "funding_policies_battle_id_key" UNIQUE ("battle_id");



ALTER TABLE ONLY "battles"."funding_policies"
    ADD CONSTRAINT "funding_policies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."generated_challenges"
    ADD CONSTRAINT "generated_challenges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."global_messages"
    ADD CONSTRAINT "global_messages_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."invitations"
    ADD CONSTRAINT "invitations_battle_lenser_unique" UNIQUE ("battle_id", "invited_lenser_id");



ALTER TABLE ONLY "battles"."invitations"
    ADD CONSTRAINT "invitations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."invite_links"
    ADD CONSTRAINT "invite_links_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."invite_links"
    ADD CONSTRAINT "invite_links_token_key" UNIQUE ("token");



ALTER TABLE ONLY "battles"."media_quality_results"
    ADD CONSTRAINT "media_quality_results_pkey" PRIMARY KEY ("submission_id");



ALTER TABLE ONLY "battles"."media_quality_rules"
    ADD CONSTRAINT "media_quality_rules_pkey" PRIMARY KEY ("template_id", "modality");



ALTER TABLE ONLY "battles"."model_test_runs"
    ADD CONSTRAINT "model_test_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."rubric_criteria"
    ADD CONSTRAINT "rubric_criteria_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."rubric_criteria"
    ADD CONSTRAINT "rubric_criteria_rubric_ordinal_unique" UNIQUE ("rubric_id", "ordinal");



ALTER TABLE ONLY "battles"."rubrics"
    ADD CONSTRAINT "rubrics_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."rule_snapshots"
    ADD CONSTRAINT "rule_snapshots_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."schedules"
    ADD CONSTRAINT "schedules_battle_id_key" UNIQUE ("battle_id");



ALTER TABLE ONLY "battles"."schedules"
    ADD CONSTRAINT "schedules_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."scorecards"
    ADD CONSTRAINT "scorecards_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."scorecards"
    ADD CONSTRAINT "scorecards_unique_per_criterion" UNIQUE ("battle_id", "contender_id", "rubric_criterion_id");



ALTER TABLE ONLY "battles"."scoring_plugin_signals"
    ADD CONSTRAINT "scoring_plugin_signals_battle_plugin_uq" UNIQUE ("battle_id", "plugin_id");



ALTER TABLE ONLY "battles"."scoring_plugin_signals"
    ADD CONSTRAINT "scoring_plugin_signals_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."series"
    ADD CONSTRAINT "series_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."series_rounds"
    ADD CONSTRAINT "series_rounds_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."series_rounds"
    ADD CONSTRAINT "series_rounds_series_id_round_number_key" UNIQUE ("series_id", "round_number");



ALTER TABLE ONLY "battles"."sponsorship_contributions"
    ADD CONSTRAINT "sponsorship_contributions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."sponsorship_payouts"
    ADD CONSTRAINT "sponsorship_payouts_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."sponsorship_pools"
    ADD CONSTRAINT "sponsorship_pools_battle_id_key" UNIQUE ("battle_id");



ALTER TABLE ONLY "battles"."sponsorship_pools"
    ADD CONSTRAINT "sponsorship_pools_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."stream_recordings"
    ADD CONSTRAINT "stream_recordings_battle_id_contender_id_key" UNIQUE ("battle_id", "contender_id");



ALTER TABLE ONLY "battles"."stream_recordings"
    ADD CONSTRAINT "stream_recordings_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_battle_contender_unique" UNIQUE ("battle_id", "contender_id");



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."template_prompt_variables"
    ADD CONSTRAINT "template_prompt_variables_pkey" PRIMARY KEY ("template_id", "variable_key");



ALTER TABLE ONLY "battles"."templates"
    ADD CONSTRAINT "templates_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."tool_registry"
    ADD CONSTRAINT "tool_registry_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."tool_registry"
    ADD CONSTRAINT "tool_registry_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "battles"."tournament_contenders"
    ADD CONSTRAINT "tournament_contenders_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."tournament_contenders"
    ADD CONSTRAINT "tournament_contenders_tournament_id_lenser_id_key" UNIQUE ("tournament_id", "lenser_id");



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."tournament_rounds"
    ADD CONSTRAINT "tournament_rounds_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."tournament_rounds"
    ADD CONSTRAINT "tournament_rounds_tournament_id_round_number_key" UNIQUE ("tournament_id", "round_number");



ALTER TABLE ONLY "battles"."tournaments"
    ADD CONSTRAINT "tournaments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."tournaments"
    ADD CONSTRAINT "tournaments_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "battles"."votes"
    ADD CONSTRAINT "uq_votes_battle_voter" UNIQUE ("battle_id", "voter_lenser_id") DEFERRABLE INITIALLY DEFERRED;



COMMENT ON CONSTRAINT "uq_votes_battle_voter" ON "battles"."votes" IS 'Prevents double-voting. DEFERRABLE so vote revision (if supported in future) can delete the old row and insert the new row within the same transaction.';



ALTER TABLE ONLY "battles"."vote_aggregates"
    ADD CONSTRAINT "vote_aggregates_pkey" PRIMARY KEY ("battle_id", "contender_id");



ALTER TABLE ONLY "battles"."vote_choices"
    ADD CONSTRAINT "vote_choices_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."votes"
    ADD CONSTRAINT "votes_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "battles"."votes"
    ADD CONSTRAINT "votes_unique_voter_per_contender" UNIQUE ("battle_id", "voter_lenser_id", "voted_contender_id");



ALTER TABLE ONLY "billing"."execution_margin_policies"
    ADD CONSTRAINT "billing_execution_margin_policies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."budget_policies"
    ADD CONSTRAINT "budget_policies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."budget_policies"
    ADD CONSTRAINT "budget_policies_scope_unique" UNIQUE ("scope", "scope_id");



ALTER TABLE ONLY "billing"."checkout_sessions"
    ADD CONSTRAINT "checkout_sessions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."cost_reservations"
    ADD CONSTRAINT "cost_reservations_idem_unique" UNIQUE ("ai_lenser_id", "idempotency_key");



ALTER TABLE ONLY "billing"."cost_reservations"
    ADD CONSTRAINT "cost_reservations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."ledger_entries"
    ADD CONSTRAINT "ledger_entries_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."orders"
    ADD CONSTRAINT "orders_ls_order_id_key" UNIQUE ("ls_order_id");



ALTER TABLE ONLY "billing"."orders"
    ADD CONSTRAINT "orders_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."pricing_snapshots"
    ADD CONSTRAINT "pricing_snapshots_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."products"
    ADD CONSTRAINT "products_ls_product_id_key" UNIQUE ("ls_product_id");



ALTER TABLE ONLY "billing"."products"
    ADD CONSTRAINT "products_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."provider_circuit_state"
    ADD CONSTRAINT "provider_circuit_state_pkey" PRIMARY KEY ("provider_key");



ALTER TABLE ONLY "billing"."runtime_settings"
    ADD CONSTRAINT "runtime_settings_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."spend_anomalies"
    ADD CONSTRAINT "spend_anomalies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."variants"
    ADD CONSTRAINT "variants_ls_variant_id_key" UNIQUE ("ls_variant_id");



ALTER TABLE ONLY "billing"."variants"
    ADD CONSTRAINT "variants_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "billing"."webhook_events"
    ADD CONSTRAINT "webhook_events_ls_event_id_key" UNIQUE ("ls_event_id");



ALTER TABLE ONLY "billing"."webhook_events"
    ADD CONSTRAINT "webhook_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "connectors"."connector_tokens"
    ADD CONSTRAINT "connector_tokens_hash_unique" UNIQUE ("token_hash");



ALTER TABLE ONLY "connectors"."connector_tokens"
    ADD CONSTRAINT "connector_tokens_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "connectors"."connectors"
    ADD CONSTRAINT "connectors_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "connectors"."connectors"
    ADD CONSTRAINT "connectors_workspace_slug_unique" UNIQUE ("workspace_id", "slug");



ALTER TABLE ONLY "content"."entity_translations"
    ADD CONSTRAINT "entity_translations_entity_lang_key" UNIQUE ("entity_type", "entity_id", "language_code");



ALTER TABLE ONLY "content"."entity_translations"
    ADD CONSTRAINT "entity_translations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."reactions"
    ADD CONSTRAINT "reactions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."reactions"
    ADD CONSTRAINT "reactions_unique_per_user" UNIQUE ("entity_type", "entity_id", "lenser_id", "reaction");



ALTER TABLE ONLY "content"."reports"
    ADD CONSTRAINT "reports_pk" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."reports"
    ADD CONSTRAINT "reports_unique" UNIQUE ("reporter_id", "target_type", "target_id");



ALTER TABLE ONLY "content"."tag_map"
    ADD CONSTRAINT "tag_map_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."tag_map"
    ADD CONSTRAINT "tag_map_unique" UNIQUE ("entity_type", "entity_id", "tag_id");



ALTER TABLE ONLY "content"."tag_suggestions"
    ADD CONSTRAINT "tag_suggestions_entity_type_entity_id_tag_id_key" UNIQUE ("entity_type", "entity_id", "tag_id");



ALTER TABLE ONLY "content"."tag_suggestions"
    ADD CONSTRAINT "tag_suggestions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."tag_translations"
    ADD CONSTRAINT "tag_translations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."tag_translations"
    ADD CONSTRAINT "tag_translations_tag_id_language_id_key" UNIQUE ("tag_id", "language_code");



ALTER TABLE ONLY "content"."tags"
    ADD CONSTRAINT "tags_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."tags"
    ADD CONSTRAINT "tags_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "content"."thread_replies"
    ADD CONSTRAINT "thread_replies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "content"."threads"
    ADD CONSTRAINT "threads_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "core"."cities"
    ADD CONSTRAINT "cities_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "core"."countries"
    ADD CONSTRAINT "countries_iso2_unique" UNIQUE ("iso2");



ALTER TABLE ONLY "core"."countries"
    ADD CONSTRAINT "countries_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "core"."currencies"
    ADD CONSTRAINT "currencies_pkey" PRIMARY KEY ("code");



ALTER TABLE ONLY "core"."languages"
    ADD CONSTRAINT "languages_code_pkey" PRIMARY KEY ("code");



ALTER TABLE ONLY "core"."regions"
    ADD CONSTRAINT "regions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "core"."states"
    ADD CONSTRAINT "states_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "core"."subregions"
    ADD CONSTRAINT "subregions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "devices"."device_challenges"
    ADD CONSTRAINT "device_challenges_nonce_key" UNIQUE ("nonce");



ALTER TABLE ONLY "devices"."device_challenges"
    ADD CONSTRAINT "device_challenges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "devices"."nonce_cache"
    ADD CONSTRAINT "nonce_cache_pkey" PRIMARY KEY ("nonce");



ALTER TABLE ONLY "devices"."peer_leases"
    ADD CONSTRAINT "peer_leases_pkey" PRIMARY KEY ("lease_kind", "lenser_id");



ALTER TABLE ONLY "devices"."registered_devices"
    ADD CONSTRAINT "registered_devices_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "devices"."sync_outbox"
    ADD CONSTRAINT "sync_outbox_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "devices"."sync_watermarks"
    ADD CONSTRAINT "sync_watermarks_pkey" PRIMARY KEY ("device_id", "object_class");



ALTER TABLE ONLY "execution"."artifact_medias"
    ADD CONSTRAINT "artifact_medias_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."artifacts"
    ADD CONSTRAINT "artifacts_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."attestation_verifications"
    ADD CONSTRAINT "attestation_verifications_pkey" PRIMARY KEY ("attestation_id");



ALTER TABLE ONLY "execution"."attestations"
    ADD CONSTRAINT "attestations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."byok_keys"
    ADD CONSTRAINT "byok_keys_agent_id_provider_key" UNIQUE ("agent_id", "provider");



ALTER TABLE ONLY "execution"."byok_keys"
    ADD CONSTRAINT "byok_keys_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."execution_tags"
    ADD CONSTRAINT "execution_tags_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."links"
    ADD CONSTRAINT "links_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."links"
    ADD CONSTRAINT "links_run_id_entity_type_entity_id_key" UNIQUE ("run_id", "entity_type", "entity_id");



ALTER TABLE ONLY "execution"."origin_types"
    ADD CONSTRAINT "origin_types_pkey" PRIMARY KEY ("key");



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."request_attachments"
    ADD CONSTRAINT "request_attachments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."request_attachments"
    ADD CONSTRAINT "request_attachments_request_key_unique" UNIQUE ("request_id", "binding_key");



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."runner_device_bindings"
    ADD CONSTRAINT "runner_device_bindings_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."runner_device_bindings"
    ADD CONSTRAINT "runner_device_bindings_runner_id_device_id_key" UNIQUE ("runner_id", "device_id");



ALTER TABLE ONLY "execution"."runners"
    ADD CONSTRAINT "runners_lenser_id_name_key" UNIQUE ("lenser_id", "name");



ALTER TABLE ONLY "execution"."runners"
    ADD CONSTRAINT "runners_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."runs"
    ADD CONSTRAINT "runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."steps"
    ADD CONSTRAINT "steps_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."stream_sessions"
    ADD CONSTRAINT "stream_sessions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."stream_sessions"
    ADD CONSTRAINT "stream_sessions_run_id_unique" UNIQUE ("run_id");



ALTER TABLE ONLY "execution"."trust_evaluations"
    ADD CONSTRAINT "trust_evaluations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "execution"."trust_evaluations"
    ADD CONSTRAINT "trust_evaluations_submission_id_key" UNIQUE ("submission_id");



ALTER TABLE ONLY "identity_gov"."reserved_namespaces"
    ADD CONSTRAINT "reserved_namespaces_kind_value_uniq" UNIQUE ("entry_kind", "value");



ALTER TABLE ONLY "identity_gov"."reserved_namespaces"
    ADD CONSTRAINT "reserved_namespaces_pk" PRIMARY KEY ("id");



ALTER TABLE ONLY "integrations"."tool_allowlist"
    ADD CONSTRAINT "agent_tool_allowlist_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "integrations"."tools"
    ADD CONSTRAINT "tools_key_unique" UNIQUE ("key");



ALTER TABLE ONLY "integrations"."tools"
    ADD CONSTRAINT "tools_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."badges"
    ADD CONSTRAINT "badges_lenser_type_unique" UNIQUE ("lenser_id", "type");



ALTER TABLE ONLY "lensers"."group_members"
    ADD CONSTRAINT "group_members_pkey" PRIMARY KEY ("group_id", "lenser_id");



ALTER TABLE ONLY "lensers"."groups"
    ADD CONSTRAINT "groups_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."groups"
    ADD CONSTRAINT "groups_slug_unique" UNIQUE ("slug");



ALTER TABLE ONLY "lensers"."badges"
    ADD CONSTRAINT "lenser_badges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."badges"
    ADD CONSTRAINT "lenser_badges_unique_per_type" UNIQUE ("lenser_id", "type");



ALTER TABLE ONLY "lensers"."social_links"
    ADD CONSTRAINT "lenser_social_links_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."profiles"
    ADD CONSTRAINT "lensers_handle_key" UNIQUE ("handle");



ALTER TABLE ONLY "lensers"."profiles"
    ADD CONSTRAINT "lensers_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_lenser_id_key" UNIQUE ("lenser_id");



ALTER TABLE ONLY "lensers"."profiles"
    ADD CONSTRAINT "profiles_user_id_unique" UNIQUE ("user_id");



ALTER TABLE ONLY "lensers"."relationships"
    ADD CONSTRAINT "relationships_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."tag_follows"
    ADD CONSTRAINT "tag_follows_pk" PRIMARY KEY ("id");



ALTER TABLE ONLY "lensers"."tag_follows"
    ADD CONSTRAINT "tag_follows_unique" UNIQUE ("lenser_id", "tag_id");



ALTER TABLE ONLY "lensers"."relationships"
    ADD CONSTRAINT "uq_relationship" UNIQUE ("source_profile_id", "target_profile_id");



ALTER TABLE ONLY "lenses"."capability_index"
    ADD CONSTRAINT "capability_index_pkey" PRIMARY KEY ("content_hash");



ALTER TABLE ONLY "lenses"."contract_channels"
    ADD CONSTRAINT "contract_channels_pkey" PRIMARY KEY ("lens_id", "channel");



ALTER TABLE ONLY "lenses"."contract_signatures"
    ADD CONSTRAINT "contract_signatures_pkey" PRIMARY KEY ("content_hash", "key_id");



ALTER TABLE ONLY "lenses"."contracts"
    ADD CONSTRAINT "contracts_pkey" PRIMARY KEY ("content_hash");



ALTER TABLE ONLY "lenses"."dependency_edges"
    ADD CONSTRAINT "dependency_edges_pkey" PRIMARY KEY ("parent_content_hash", "child_content_hash", "binding");



ALTER TABLE ONLY "lenses"."execution_records"
    ADD CONSTRAINT "execution_records_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."steps"
    ADD CONSTRAINT "lens_steps_lens_id_ordinal_key" UNIQUE ("lens_id", "ordinal");



ALTER TABLE ONLY "lenses"."steps"
    ADD CONSTRAINT "lens_steps_pkey" PRIMARY KEY ("id");



ALTER TABLE "lenses"."steps"
    ADD CONSTRAINT "lens_steps_sub_lens_required" CHECK ((("step_type" <> 'lens'::"text") OR ("sub_lens_id" IS NOT NULL))) NOT VALID;



ALTER TABLE ONLY "lenses"."version_parameters"
    ADD CONSTRAINT "lens_version_params_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."versions"
    ADD CONSTRAINT "lens_versions_number_unique" UNIQUE ("lens_id", "version_number");



ALTER TABLE ONLY "lenses"."versions"
    ADD CONSTRAINT "lens_versions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."lenses"
    ADD CONSTRAINT "lenses_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."parameter_contracts"
    ADD CONSTRAINT "parameter_contracts_pkey" PRIMARY KEY ("content_hash", "label");



ALTER TABLE ONLY "lenses"."parameter_deprecations"
    ADD CONSTRAINT "parameter_deprecations_pkey" PRIMARY KEY ("from_content_hash", "label");



ALTER TABLE ONLY "lenses"."schedule_calendars"
    ADD CONSTRAINT "schedule_calendars_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."security_scopes"
    ADD CONSTRAINT "security_scopes_pkey" PRIMARY KEY ("content_hash", "required_scope");



ALTER TABLE ONLY "lenses"."tools"
    ADD CONSTRAINT "tools_key_key" UNIQUE ("key");



ALTER TABLE ONLY "lenses"."tools"
    ADD CONSTRAINT "tools_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."tutorial_completions"
    ADD CONSTRAINT "tutorial_completions_lenser_slug_kind_unique" UNIQUE ("lenser_id", "tutorial_slug", "kind");



ALTER TABLE ONLY "lenses"."tutorial_completions"
    ADD CONSTRAINT "tutorial_completions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."version_parameter_contents"
    ADD CONSTRAINT "uq_param_workspace_lenser" UNIQUE ("parameter_id", "workspace_id", "lenser_id");



ALTER TABLE ONLY "lenses"."version_parameter_contents"
    ADD CONSTRAINT "version_parameter_contents_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_edges"
    ADD CONSTRAINT "wf_edges_unique_connection" UNIQUE ("source_node_id", "target_node_id", "target_param_label");



ALTER TABLE ONLY "lenses"."workflow_edges"
    ADD CONSTRAINT "workflow_edges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "workflow_node_results_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "workflow_node_results_run_id_node_id_key" UNIQUE ("run_id", "node_id");



ALTER TABLE ONLY "lenses"."workflow_nodes"
    ADD CONSTRAINT "workflow_nodes_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_phases"
    ADD CONSTRAINT "workflow_phases_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_run_chains"
    ADD CONSTRAINT "workflow_run_chains_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_run_dead_letters"
    ADD CONSTRAINT "workflow_run_dead_letters_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_run_events"
    ADD CONSTRAINT "workflow_run_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_run_events"
    ADD CONSTRAINT "workflow_run_events_run_event_unique" UNIQUE ("run_id", "event_id");



ALTER TABLE ONLY "lenses"."workflow_run_provenance"
    ADD CONSTRAINT "workflow_run_provenance_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_run_provenance"
    ADD CONSTRAINT "workflow_run_provenance_unique" UNIQUE ("source_run_id", "source_node_id", "source_output_path", "target_run_id", "target_node_id", "target_input_path");



ALTER TABLE ONLY "lenses"."workflow_run_tags"
    ADD CONSTRAINT "workflow_run_tags_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "workflow_runs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_schedules"
    ADD CONSTRAINT "workflow_schedules_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_tasks"
    ADD CONSTRAINT "workflow_tasks_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_triggers"
    ADD CONSTRAINT "workflow_triggers_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_version_edges"
    ADD CONSTRAINT "workflow_version_edges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_version_nodes"
    ADD CONSTRAINT "workflow_version_nodes_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_versions"
    ADD CONSTRAINT "workflow_versions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "lenses"."workflow_versions"
    ADD CONSTRAINT "workflow_versions_workflow_version_key" UNIQUE ("workflow_id", "version_number");



ALTER TABLE ONLY "lenses"."workflows"
    ADD CONSTRAINT "workflows_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "media"."attachments"
    ADD CONSTRAINT "attachments_entity_binding_unique" UNIQUE ("entity_type", "entity_id", "binding_key");



ALTER TABLE ONLY "media"."attachments"
    ADD CONSTRAINT "attachments_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "media"."objects"
    ADD CONSTRAINT "objects_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "organizations"."activity_log"
    ADD CONSTRAINT "activity_log_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "organizations"."addresses"
    ADD CONSTRAINT "addresses_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "organizations"."audit_logs"
    ADD CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "organizations"."companies"
    ADD CONSTRAINT "companies_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "organizations"."members"
    ADD CONSTRAINT "members_pkey" PRIMARY KEY ("org_id", "lenser_id");



ALTER TABLE ONLY "organizations"."organizations"
    ADD CONSTRAINT "organizations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "organizations"."preferences"
    ADD CONSTRAINT "preferences_pkey" PRIMARY KEY ("org_id");



ALTER TABLE ONLY "organizations"."companies"
    ADD CONSTRAINT "uq_companies_org" UNIQUE ("org_id");



ALTER TABLE ONLY "organizations"."organizations"
    ADD CONSTRAINT "uq_organizations_slug" UNIQUE ("slug");



ALTER TABLE ONLY "platform"."api_worker_heartbeats"
    ADD CONSTRAINT "api_worker_heartbeats_pkey" PRIMARY KEY ("worker_id");



ALTER TABLE ONLY "platform"."system_flags"
    ADD CONSTRAINT "system_flags_pkey" PRIMARY KEY ("key");



ALTER TABLE ONLY "platform"."tool_invocation_logs"
    ADD CONSTRAINT "tool_invocation_logs_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_recipient_id_notification_type_enti_key" UNIQUE ("recipient_id", "notification_type", "entity_id", "window_start");



ALTER TABLE ONLY "public"."notification_preferences"
    ADD CONSTRAINT "notification_preferences_lenser_id_notification_type_key" UNIQUE ("lenser_id", "notification_type");



ALTER TABLE ONLY "public"."notification_preferences"
    ADD CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."notifications"
    ADD CONSTRAINT "notifications_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "uoc_unique_ref_per_lenser" UNIQUE ("lenser_id", "workspace_id", "ref");



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "user_oauth_connections_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."workflow_integration_credentials"
    ADD CONSTRAINT "workflow_integration_credentials_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."workflow_kv_store"
    ADD CONSTRAINT "workflow_kv_store_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."workflow_webhook_triggers"
    ADD CONSTRAINT "workflow_webhook_triggers_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "reputation"."contender_ratings"
    ADD CONSTRAINT "contender_ratings_lenser_id_category_key" UNIQUE ("lenser_id", "category");



ALTER TABLE ONLY "reputation"."contender_ratings"
    ADD CONSTRAINT "contender_ratings_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "reputation"."elo_battle_log"
    ADD CONSTRAINT "elo_battle_log_pkey" PRIMARY KEY ("battle_id");



ALTER TABLE ONLY "reputation"."judge_calibrations"
    ADD CONSTRAINT "judge_calibrations_lenser_id_key" UNIQUE ("lenser_id");



ALTER TABLE ONLY "reputation"."judge_calibrations"
    ADD CONSTRAINT "judge_calibrations_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "reputation"."lenser_scores"
    ADD CONSTRAINT "lenser_scores_lenser_id_score_type_key" UNIQUE ("lenser_id", "score_type");



ALTER TABLE ONLY "reputation"."lenser_scores"
    ADD CONSTRAINT "lenser_scores_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "reputation"."vote_risk_scores"
    ADD CONSTRAINT "vote_risk_scores_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "reputation"."vote_risk_scores"
    ADD CONSTRAINT "vote_risk_scores_vote_id_key" UNIQUE ("vote_id");



ALTER TABLE ONLY "status"."components"
    ADD CONSTRAINT "components_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "status"."components"
    ADD CONSTRAINT "components_service_slug_key" UNIQUE ("service_id", "slug");



ALTER TABLE ONLY "status"."daily_snapshots"
    ADD CONSTRAINT "daily_snapshots_pkey" PRIMARY KEY ("snapshot_date", "scope_type", "scope_key");



ALTER TABLE ONLY "status"."incident_updates"
    ADD CONSTRAINT "incident_updates_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "status"."incidents"
    ADD CONSTRAINT "incidents_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "status"."incidents"
    ADD CONSTRAINT "incidents_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "status"."maintenance_windows"
    ADD CONSTRAINT "maintenance_windows_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "status"."maintenance_windows"
    ADD CONSTRAINT "maintenance_windows_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "status"."services"
    ADD CONSTRAINT "services_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "status"."services"
    ADD CONSTRAINT "services_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "status"."uptime_rollups"
    ADD CONSTRAINT "uptime_rollups_pkey" PRIMARY KEY ("scope_type", "scope_key", "rollup_window");



ALTER TABLE ONLY "tenancy"."workspace_members"
    ADD CONSTRAINT "workspace_members_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "tenancy"."workspace_members"
    ADD CONSTRAINT "workspace_members_unique" UNIQUE ("workspace_id", "lenser_id");



ALTER TABLE ONLY "tenancy"."workspaces"
    ADD CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "tenancy"."workspaces"
    ADD CONSTRAINT "workspaces_slug_unique" UNIQUE ("slug");



ALTER TABLE ONLY "wallet"."accounts"
    ADD CONSTRAINT "accounts_lenser_id_key" UNIQUE ("lenser_id");



ALTER TABLE ONLY "wallet"."accounts"
    ADD CONSTRAINT "accounts_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "wallet"."credit_valuation_policy"
    ADD CONSTRAINT "credit_valuation_policy_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "wallet"."credit_valuation_policy"
    ADD CONSTRAINT "credit_valuation_singleton" UNIQUE ("singleton");



ALTER TABLE ONLY "wallet"."organization_accounts"
    ADD CONSTRAINT "organization_accounts_org_id_key" UNIQUE ("org_id");



ALTER TABLE ONLY "wallet"."organization_accounts"
    ADD CONSTRAINT "organization_accounts_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "wallet"."pending_charges"
    ADD CONSTRAINT "pending_charges_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "wallet"."pending_charges"
    ADD CONSTRAINT "pending_charges_reference_unique" UNIQUE ("reference_id");



ALTER TABLE ONLY "wallet"."spending_limits"
    ADD CONSTRAINT "spending_limits_account_period_key" UNIQUE ("account_id", "period");



ALTER TABLE ONLY "wallet"."spending_limits"
    ADD CONSTRAINT "spending_limits_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "wallet"."transactions"
    ADD CONSTRAINT "transactions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."apps"
    ADD CONSTRAINT "apps_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."apps"
    ADD CONSTRAINT "apps_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "xp"."contributions"
    ADD CONSTRAINT "contributions_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."difficulty_multipliers"
    ADD CONSTRAINT "difficulty_multipliers_pkey" PRIMARY KEY ("difficulty");



ALTER TABLE ONLY "xp"."seasons"
    ADD CONSTRAINT "excl_no_overlapping_active_seasons" EXCLUDE USING "gist" ("app_id" WITH =, "tstzrange"("starts_at", "ends_at") WITH &&) WHERE (("is_active" = true));



ALTER TABLE ONLY "xp"."level_ups"
    ADD CONSTRAINT "lenser_level_ups_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."event_verifications"
    ADD CONSTRAINT "xp_event_verifications_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."events"
    ADD CONSTRAINT "xp_events_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."levels"
    ADD CONSTRAINT "xp_levels_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."monthly_rollup"
    ADD CONSTRAINT "xp_monthly_rollup_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."monthly_rollup"
    ADD CONSTRAINT "xp_monthly_rollup_unique" UNIQUE ("lenser_id", "year", "month");



ALTER TABLE ONLY "xp"."policy"
    ADD CONSTRAINT "xp_policy_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."rules"
    ADD CONSTRAINT "xp_rules_app_action_key" UNIQUE ("app_id", "action_key");



ALTER TABLE ONLY "xp"."rules"
    ADD CONSTRAINT "xp_rules_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."season_totals"
    ADD CONSTRAINT "xp_season_totals_pkey" PRIMARY KEY ("season_id", "lenser_id", "app_id");



ALTER TABLE ONLY "xp"."seasons"
    ADD CONSTRAINT "xp_seasons_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "xp"."seasons"
    ADD CONSTRAINT "xp_seasons_slug_key" UNIQUE ("slug");



ALTER TABLE ONLY "xp"."streaks"
    ADD CONSTRAINT "xp_streaks_pkey" PRIMARY KEY ("lenser_id", "app_id", "streak_type");



ALTER TABLE ONLY "xp"."totals"
    ADD CONSTRAINT "xp_totals_pkey" PRIMARY KEY ("lenser_id", "app_id");



CREATE INDEX "idx_kill_switches_active" ON "admin"."kill_switches" USING "btree" ("scope", "target_id") WHERE ("lifted_at" IS NULL);



CREATE INDEX "idx_agent_action_logs_context" ON "agents"."action_logs" USING "btree" ("context_ref_type", "context_ref_id") WHERE ("context_ref_id" IS NOT NULL);



CREATE INDEX "idx_agent_action_logs_lenser_time" ON "agents"."action_logs" USING "btree" ("ai_lenser_id", "occurred_at" DESC);



CREATE UNIQUE INDEX "idx_agent_lens_default_instruction" ON "agents"."lens_bindings" USING "btree" ("ai_lenser_id") WHERE (("is_default" = true) AND (NOT ('personality'::"text" = ANY ("category_tags"))));



CREATE UNIQUE INDEX "idx_agent_lens_default_personality" ON "agents"."lens_bindings" USING "btree" ("ai_lenser_id") WHERE (("is_default" = true) AND ('personality'::"text" = ANY ("category_tags")));



CREATE UNIQUE INDEX "idx_agent_model_one_default" ON "agents"."model_bindings" USING "btree" ("ai_lenser_id") WHERE ("is_default" = true);



CREATE INDEX "idx_agents_agent_run_events_team_run" ON "agents"."agent_run_events" USING "btree" ("team_run_id", "occurred_at" DESC);



CREATE INDEX "idx_agents_agent_run_steps_team_run" ON "agents"."agent_run_steps" USING "btree" ("team_run_id", "lane", "created_at");



CREATE INDEX "idx_agents_lifecycle_profile" ON "agents"."ai_lensers" USING "btree" ("profile_id", "is_active", "archived_at", "deleted_at");



CREATE INDEX "idx_agents_memories_agent_expires" ON "agents"."memories" USING "btree" ("ai_lenser_id", "expires_at");



CREATE INDEX "idx_agents_memories_fts" ON "agents"."memories" USING "gin" ("to_tsvector"('"english"'::"regconfig", "content"));



COMMENT ON INDEX "agents"."idx_agents_memories_fts" IS 'Phase N1: GIN index on to_tsvector(english, content) for full-text search via fn_search_memory_entries.';



CREATE INDEX "idx_agents_memories_profile_scope" ON "agents"."memories" USING "btree" ("profile_id", "scope", "created_at" DESC);



CREATE INDEX "idx_agents_memory_access_logs_memory" ON "agents"."memory_access_logs" USING "btree" ("memory_id", "accessed_at" DESC);



CREATE INDEX "idx_agents_ownerships_ai" ON "agents"."ownerships" USING "btree" ("ai_lenser_id");



CREATE INDEX "idx_agents_ownerships_owner" ON "agents"."ownerships" USING "btree" ("owner_lenser_id") WHERE ("revoked_at" IS NULL);



CREATE UNIQUE INDEX "idx_agents_ownerships_primary" ON "agents"."ownerships" USING "btree" ("ai_lenser_id") WHERE (("role" = 'owner'::"text") AND ("revoked_at" IS NULL));



CREATE INDEX "idx_agents_team_members_team" ON "agents"."team_members" USING "btree" ("team_id", "sort_order", "lane");



CREATE INDEX "idx_agents_team_runs_ai_lenser" ON "agents"."team_runs" USING "btree" ("ai_lenser_id", "created_at" DESC);



CREATE INDEX "idx_agents_teams_ai_lenser" ON "agents"."teams" USING "btree" ("ai_lenser_id", "updated_at" DESC);



CREATE INDEX "idx_agents_workflow_assignments_ai_lenser" ON "agents"."workflow_assignments" USING "btree" ("ai_lenser_id", "workflow_id");



CREATE INDEX "idx_battle_subscriptions_agent" ON "agents"."battle_subscriptions" USING "btree" ("agent_id") WHERE ("active" = true);



CREATE INDEX "idx_evaluation_case_results_run" ON "agents"."evaluation_case_results" USING "btree" ("evaluation_run_id");



CREATE INDEX "idx_evaluation_cases_eval" ON "agents"."evaluation_cases" USING "btree" ("evaluation_id");



CREATE INDEX "idx_evaluation_rubrics_eval" ON "agents"."evaluation_rubrics" USING "btree" ("evaluation_id", "version" DESC);



CREATE INDEX "idx_evaluation_runs_eval" ON "agents"."evaluation_runs" USING "btree" ("evaluation_id", "started_at" DESC);



CREATE INDEX "idx_evaluations_ai_lenser" ON "agents"."evaluations" USING "btree" ("ai_lenser_id");



CREATE INDEX "idx_evaluations_owner" ON "agents"."evaluations" USING "btree" ("owner_lenser_id");



CREATE INDEX "idx_fk_lens_bindings_lens_id" ON "agents"."lens_bindings" USING "btree" ("lens_id");



CREATE INDEX "idx_fk_lens_bindings_version_id" ON "agents"."lens_bindings" USING "btree" ("version_id");



CREATE INDEX "idx_fk_model_bindings_model_id" ON "agents"."model_bindings" USING "btree" ("model_id");



CREATE INDEX "idx_fk_ownerships_owner_lenser_id" ON "agents"."ownerships" USING "btree" ("owner_lenser_id");



CREATE INDEX "idx_gateway_commands_pending" ON "agents"."gateway_commands" USING "btree" ("device_id", "created_at") WHERE ("claimed_at" IS NULL);



CREATE INDEX "idx_gateway_commands_unacked" ON "agents"."gateway_commands" USING "btree" ("device_id") WHERE ("acked_at" IS NULL);



CREATE INDEX "idx_gateway_devices_last_seen" ON "agents"."gateway_devices" USING "btree" ("last_seen_at" DESC NULLS LAST);



CREATE INDEX "idx_gateway_devices_owner" ON "agents"."gateway_devices" USING "btree" ("owner_id");



CREATE INDEX "idx_model_bindings_ai_lenser_id" ON "agents"."model_bindings" USING "btree" ("ai_lenser_id");



CREATE INDEX "idx_policy_evals_agent_at" ON "agents"."policy_evaluations" USING "btree" ("ai_lenser_id", "evaluated_at" DESC);



CREATE INDEX "idx_policy_evals_deny" ON "agents"."policy_evaluations" USING "btree" ("ai_lenser_id", "evaluated_at" DESC) WHERE ("verdict" = 'deny'::"text");



CREATE INDEX "idx_policy_evals_team_run" ON "agents"."policy_evaluations" USING "btree" ("team_run_id") WHERE ("team_run_id" IS NOT NULL);



CREATE INDEX "idx_provider_configs_ai_lenser" ON "agents"."provider_configs" USING "btree" ("ai_lenser_id");



CREATE INDEX "idx_run_incidents_agent_type" ON "agents"."run_incidents" USING "btree" ("ai_lenser_id", "incident_type");



CREATE INDEX "idx_run_incidents_report" ON "agents"."run_incidents" USING "btree" ("run_report_id", "created_at" DESC);



CREATE INDEX "idx_run_incidents_severity" ON "agents"."run_incidents" USING "btree" ("severity", "created_at" DESC);



CREATE INDEX "idx_run_reports_ai_lenser" ON "agents"."run_reports" USING "btree" ("ai_lenser_id", "created_at" DESC);



CREATE INDEX "idx_run_reports_outcome" ON "agents"."run_reports" USING "btree" ("outcome", "created_at" DESC);



CREATE INDEX "idx_run_reports_team_run" ON "agents"."run_reports" USING "btree" ("team_run_id") WHERE ("team_run_id" IS NOT NULL);



CREATE INDEX "idx_scratchpad_runs_ai_lenser_started" ON "agents"."scratchpad_runs" USING "btree" ("ai_lenser_id", "started_at" DESC);



CREATE INDEX "idx_standing_approvals_active" ON "agents"."standing_approvals" USING "btree" ("ai_lenser_id", "workflow_id", "gate_kind") WHERE ("revoked_at" IS NULL);



CREATE INDEX "idx_team_messages_team_run_occurred" ON "agents"."team_messages" USING "btree" ("team_run_id", "occurred_at" DESC);



CREATE INDEX "idx_team_messages_team_run_parent" ON "agents"."team_messages" USING "btree" ("team_run_id", "parent_message_id");



CREATE INDEX "idx_team_messages_to_agent" ON "agents"."team_messages" USING "btree" ("to_agent_id") WHERE ("to_agent_id" IS NOT NULL);



CREATE INDEX "idx_team_runs_agent_snapshot" ON "agents"."team_runs" USING "btree" ("ai_lenser_id", "redacted_agent_snapshot_hash");



CREATE INDEX "idx_team_runs_parent" ON "agents"."team_runs" USING "btree" ("parent_team_run_id") WHERE ("parent_team_run_id" IS NOT NULL);



CREATE INDEX "idx_tool_assignments_agent" ON "agents"."tool_assignments" USING "btree" ("ai_lenser_id");



CREATE INDEX "idx_tool_invocations_agent_status" ON "agents"."tool_invocations" USING "btree" ("ai_lenser_id", "status");



CREATE INDEX "idx_tool_invocations_pending_approval" ON "agents"."tool_invocations" USING "btree" ("ai_lenser_id", "created_at" DESC) WHERE ("approval_status" = 'pending'::"text");



CREATE INDEX "idx_tool_invocations_team_run" ON "agents"."tool_invocations" USING "btree" ("team_run_id", "created_at" DESC);



CREATE INDEX "idx_tool_invocations_tool" ON "agents"."tool_invocations" USING "btree" ("tool_id", "created_at" DESC);



CREATE INDEX "idx_tools_registry_egress" ON "agents"."tools_registry" USING "btree" ("egress_class");



CREATE INDEX "idx_tools_registry_owner" ON "agents"."tools_registry" USING "btree" ("owner_lenser_id");



CREATE INDEX "idx_workspace_switches_human" ON "agents"."workspace_switches" USING "btree" ("human_lenser_id", "switched_at" DESC);



CREATE UNIQUE INDEX "idx_ai_model_pricing_active_unique" ON "ai"."model_pricing" USING "btree" ("model_id") WHERE ("effective_to" IS NULL);



CREATE INDEX "idx_ai_model_pricing_model_active" ON "ai"."model_pricing" USING "btree" ("model_id") WHERE ("effective_to" IS NULL);



CREATE INDEX "idx_ai_models_active" ON "ai"."models" USING "btree" ("is_active", "provider_id") WHERE ("is_active" = true);



CREATE UNIQUE INDEX "idx_ai_models_key" ON "ai"."models" USING "btree" ("key") WHERE ("key" IS NOT NULL);



CREATE INDEX "idx_ai_models_key_active" ON "ai"."models" USING "btree" ("key") WHERE ("is_active" = true);



COMMENT ON INDEX "ai"."idx_ai_models_key_active" IS 'Partial index for active model_key lookups — used in get_model_info and execution routing.';



CREATE INDEX "idx_ai_models_provider_active" ON "ai"."models" USING "btree" ("provider_id", "is_active") WHERE ("is_active" = true);



COMMENT ON INDEX "ai"."idx_ai_models_provider_active" IS 'Partial index for (provider_id, is_active=true) — used in cascade trigger + get_active_models_by_provider.';



CREATE INDEX "idx_ai_models_provider_id" ON "ai"."models" USING "btree" ("provider_id") WHERE ("provider_id" IS NOT NULL);



CREATE INDEX "idx_ai_providers_is_active" ON "ai"."providers" USING "btree" ("is_active") WHERE ("is_active" = true);



COMMENT ON INDEX "ai"."idx_ai_providers_is_active" IS 'Partial index to accelerate active-provider scans used by public functions.';



CREATE INDEX "idx_fk_keys_provider_id" ON "ai"."keys" USING "btree" ("provider_id");



CREATE INDEX "idx_key_usage_log_key_created" ON "ai"."key_usage_log" USING "btree" ("key_id", "created_at" DESC);



CREATE INDEX "idx_key_usage_log_lenser_created" ON "ai"."key_usage_log" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_key_usage_log_run" ON "ai"."key_usage_log" USING "btree" ("run_id") WHERE ("run_id" IS NOT NULL);



CREATE INDEX "idx_key_usage_log_workspace_created" ON "ai"."key_usage_log" USING "btree" ("workspace_id", "created_at" DESC) WHERE ("workspace_id" IS NOT NULL);



CREATE INDEX "idx_keys_expires_at" ON "ai"."keys" USING "btree" ("expires_at") WHERE ("expires_at" IS NOT NULL);



CREATE INDEX "idx_keys_lenser_active" ON "ai"."keys" USING "btree" ("lenser_id") WHERE ("is_active" = true);



CREATE INDEX "idx_keys_scope" ON "ai"."keys" USING "btree" ("scope") WHERE ("scope" <> 'user'::"ai"."key_scope_enum");



CREATE INDEX "idx_keys_status" ON "ai"."keys" USING "btree" ("status");



CREATE INDEX "idx_model_pricing_model_active" ON "ai"."model_pricing" USING "btree" ("model_id", "effective_from" DESC) WHERE ("effective_to" IS NULL);



CREATE INDEX "idx_model_pricing_unit_type" ON "ai"."model_pricing" USING "btree" ("unit_type");



CREATE INDEX "acd_lenser_date_idx" ON "analytics"."agent_cost_daily" USING "btree" ("ai_lenser_id", "period_date" DESC);



CREATE INDEX "eqd_lenser_date_idx" ON "analytics"."eval_quality_daily" USING "btree" ("ai_lenser_id", "period_date" DESC);



CREATE INDEX "idx_fk_contact_lenser_id" ON "analytics"."contact" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_page_views_lenser_id" ON "analytics"."page_views" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_page_views_user_id" ON "analytics"."page_views" USING "btree" ("user_id");



CREATE INDEX "idx_fk_product_feedback_user_id" ON "analytics"."product_feedback" USING "btree" ("user_id");



CREATE INDEX "idx_fk_share_events_shared_link_id" ON "analytics"."share_events" USING "btree" ("shared_link_id");



CREATE INDEX "idx_fk_share_events_viewer_lenser_id" ON "analytics"."share_events" USING "btree" ("viewer_lenser_id");



CREATE INDEX "idx_fk_shared_links_creator_lenser_id" ON "analytics"."shared_links" USING "btree" ("creator_lenser_id");



CREATE INDEX "idx_fk_tag_activity_events_actor_id" ON "analytics"."tag_activity_events" USING "btree" ("actor_id");



CREATE INDEX "idx_lenser_activity_type_created" ON "analytics"."lenser_activity" USING "btree" ("lenser_id", "activity_type", "created_at" DESC);



CREATE INDEX "idx_shared_links_creator" ON "analytics"."shared_links" USING "btree" ("creator_lenser_id");



CREATE INDEX "idx_shared_links_expires_at" ON "analytics"."shared_links" USING "btree" ("expires_at") WHERE (("expires_at" IS NOT NULL) AND ("is_active" = true));



CREATE INDEX "idx_shared_links_resource_type_active" ON "analytics"."shared_links" USING "btree" ("resource_type", "is_active") WHERE ("is_active" = true);



CREATE INDEX "idx_tag_activity_events_tag_id" ON "analytics"."tag_activity_events" USING "btree" ("tag_id");



CREATE INDEX "idx_tag_activity_events_tag_occurred" ON "analytics"."tag_activity_events" USING "btree" ("tag_id", "occurred_at");



CREATE INDEX "lenser_engagement_totals_lenser_id_idx" ON "analytics"."lenser_stats" USING "btree" ("lenser_id");



CREATE INDEX "tag_activity_events_entity_idx" ON "analytics"."tag_activity_events" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "wpd_lenser_date_idx" ON "analytics"."workflow_perf_daily" USING "btree" ("ai_lenser_id", "period_date" DESC);



CREATE INDEX "idx_account_lifecycle_audit_profile_created_at" ON "audit"."account_lifecycle" USING "btree" ("profile_id", "created_at" DESC);



CREATE INDEX "idx_account_lifecycle_audit_user_created_at" ON "audit"."account_lifecycle" USING "btree" ("user_id", "created_at" DESC);



CREATE INDEX "idx_audit_admin_actions_admin" ON "audit"."admin_actions" USING "btree" ("admin_lenser_id", "occurred_at" DESC);



CREATE INDEX "idx_audit_admin_actions_target" ON "audit"."admin_actions" USING "btree" ("target_entity_type", "target_entity_id");



CREATE INDEX "idx_audit_admin_actions_type" ON "audit"."admin_actions" USING "btree" ("action_type", "occurred_at" DESC);



CREATE INDEX "idx_audit_attestations_entity" ON "audit"."attestations" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_audit_attestations_type_time" ON "audit"."attestations" USING "btree" ("attestation_type", "created_at" DESC);



CREATE INDEX "idx_audit_disputes_entity" ON "audit"."disputes" USING "btree" ("target_entity_type", "target_entity_id");



CREATE INDEX "idx_audit_disputes_open" ON "audit"."disputes" USING "btree" ("disputer_lenser_id", "created_at" DESC) WHERE ("status" = 'open'::"text");



CREATE INDEX "idx_audit_events_actor" ON "audit"."events" USING "btree" ("actor_lenser_id", "occurred_at" DESC) WHERE ("actor_lenser_id" IS NOT NULL);



CREATE INDEX "idx_audit_events_entity" ON "audit"."events" USING "btree" ("entity_schema", "entity_table", "entity_id");



CREATE INDEX "idx_audit_events_entity_time" ON "audit"."events" USING "btree" ("entity_id", "occurred_at" DESC) WHERE ("entity_id" IS NOT NULL);



CREATE INDEX "idx_audit_events_severity" ON "audit"."events" USING "btree" ("severity", "occurred_at" DESC) WHERE ("severity" = ANY (ARRAY['warn'::"text", 'error'::"text", 'critical'::"text"]));



CREATE INDEX "idx_audit_events_type_time" ON "audit"."events" USING "btree" ("event_type", "occurred_at" DESC);



CREATE INDEX "idx_audit_events_unified_actor" ON "audit"."events" USING "btree" ("actor_type", "actor_id", "occurred_at" DESC) WHERE ("actor_id" IS NOT NULL);



CREATE INDEX "idx_audit_hash_chain_event" ON "audit"."hash_chains" USING "btree" ("event_id");



CREATE UNIQUE INDEX "idx_audit_hash_chain_seq" ON "audit"."hash_chains" USING "btree" ("sequence_no");



CREATE INDEX "idx_audit_mod_decisions_moderator" ON "audit"."moderation_decisions" USING "btree" ("moderator_lenser_id", "occurred_at" DESC) WHERE ("moderator_lenser_id" IS NOT NULL);



CREATE INDEX "idx_audit_mod_decisions_target" ON "audit"."moderation_decisions" USING "btree" ("target_entity_schema", "target_entity_table", "target_entity_id");



CREATE INDEX "idx_audit_mod_decisions_type_time" ON "audit"."moderation_decisions" USING "btree" ("decision_type", "occurred_at" DESC);



CREATE INDEX "idx_audit_security_events_critical" ON "audit"."security_events" USING "btree" ("occurred_at" DESC) WHERE ("severity" = ANY (ARRAY['error'::"text", 'critical'::"text"]));



CREATE INDEX "idx_audit_security_events_key_ref" ON "audit"."security_events" USING "btree" ("key_ref_id", "occurred_at" DESC) WHERE ("key_ref_id" IS NOT NULL);



CREATE INDEX "idx_audit_security_events_lenser" ON "audit"."security_events" USING "btree" ("lenser_id", "occurred_at" DESC) WHERE ("lenser_id" IS NOT NULL);



CREATE INDEX "idx_audit_security_events_type" ON "audit"."security_events" USING "btree" ("event_type", "occurred_at" DESC);



CREATE INDEX "idx_byok_key_usage_battle_id" ON "audit"."byok_key_usage" USING "btree" ("battle_id");



CREATE INDEX "idx_byok_key_usage_called_at" ON "audit"."byok_key_usage" USING "btree" ("called_at" DESC);



CREATE INDEX "idx_byok_key_usage_key_id" ON "audit"."byok_key_usage" USING "btree" ("key_id");



CREATE INDEX "idx_fk_attestations_attested_by" ON "audit"."attestations" USING "btree" ("attested_by");



CREATE INDEX "idx_fk_disputes_resolved_by" ON "audit"."disputes" USING "btree" ("resolved_by");



CREATE INDEX "idx_hash_chains_kind_subject_seq" ON "audit"."hash_chains" USING "btree" ("chain_kind", "subject_lenser_id", "sub_sequence_no" DESC) WHERE ("chain_kind" = 'gateway'::"text");



CREATE INDEX "idx_webhook_outbox_pending_due" ON "audit"."webhook_outbox" USING "btree" ("next_attempt_at") WHERE (("delivered_at" IS NULL) AND ("dead_lettered_at" IS NULL));



CREATE INDEX "idx_developer_tokens_lenser_id_created_at" ON "authz"."developer_tokens" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_developer_tokens_revoked_at" ON "authz"."developer_tokens" USING "btree" ("revoked_at");



CREATE INDEX "idx_developer_tokens_status_expires_at" ON "authz"."developer_tokens" USING "btree" ("status", "expires_at");



CREATE INDEX "idx_device_approval_requests_approved_by_user_id" ON "authz"."device_approval_requests" USING "btree" ("approved_by_user_id");



CREATE INDEX "idx_device_approval_requests_requested_by_user_id" ON "authz"."device_approval_requests" USING "btree" ("requested_by_user_id");



CREATE INDEX "idx_device_approval_requests_status_expires_at" ON "authz"."device_approval_requests" USING "btree" ("status", "expires_at");



CREATE INDEX "idx_automation_events_source_id" ON "automation"."events" USING "btree" ("source_id") WHERE ("source_id" IS NOT NULL);



CREATE INDEX "idx_automation_events_source_lenser" ON "automation"."events" USING "btree" ("source_lenser_id") WHERE ("source_lenser_id" IS NOT NULL);



CREATE INDEX "idx_automation_events_type_time" ON "automation"."events" USING "btree" ("event_type", "occurred_at" DESC);



CREATE INDEX "idx_cron_runs_job_started" ON "automation"."cron_runs" USING "btree" ("job_name", "started_at" DESC);



CREATE INDEX "idx_event_dispatches_status" ON "automation"."event_dispatches" USING "btree" ("status", "attempted_at");



CREATE INDEX "idx_trigger_rules_event_type_active" ON "automation"."trigger_rules" USING "btree" ("match_event_type") WHERE "is_active";



CREATE INDEX "idx_trigger_rules_lenser_active" ON "automation"."trigger_rules" USING "btree" ("lenser_id", "is_active");



CREATE INDEX "battles_battle_type_idx" ON "battles"."battles" USING "btree" ("battle_type");



CREATE INDEX "battles_lens_id_idx" ON "battles"."battles" USING "btree" ("lens_id") WHERE ("lens_id" IS NOT NULL);



CREATE INDEX "battles_workflow_id_idx" ON "battles"."battles" USING "btree" ("workflow_id") WHERE ("workflow_id" IS NOT NULL);



CREATE INDEX "idx_ai_judge_verdicts_battle" ON "battles"."ai_judge_verdicts" USING "btree" ("battle_id", "contender_id");



CREATE INDEX "idx_battle_dlq_unresolved" ON "battles"."battle_execution_dead_letters" USING "btree" ("created_at" DESC) WHERE ("resolved_at" IS NULL);



CREATE INDEX "idx_battle_entity_map_agent_snapshot" ON "battles"."contender_entity_map" USING "btree" ("ai_lenser_id", "redacted_agent_snapshot_hash");



CREATE INDEX "idx_battle_events_battle_time" ON "battles"."events" USING "btree" ("battle_id", "created_at" DESC);



CREATE INDEX "idx_battle_execution_jobs_battle" ON "battles"."battle_execution_jobs" USING "btree" ("battle_id");



CREATE INDEX "idx_battle_execution_jobs_queued" ON "battles"."battle_execution_jobs" USING "btree" ("created_at") WHERE ("status" = 'queued'::"text");



CREATE INDEX "idx_battle_rule_snapshots_battle" ON "battles"."rule_snapshots" USING "btree" ("battle_id", "created_at" DESC);



CREATE INDEX "idx_battles_benchmark_export" ON "battles"."battles" USING "btree" ((("benchmark_config" ->> 'game_key'::"text"))) WHERE ("benchmark_config" IS NOT NULL);



CREATE INDEX "idx_battles_lifecycle_creator" ON "battles"."battles" USING "btree" ("creator_lenser_id", "status", "archived_at", "deleted_at");



CREATE INDEX "idx_battles_parent_battle_id" ON "battles"."battles" USING "btree" ("parent_battle_id") WHERE ("parent_battle_id" IS NOT NULL);



CREATE INDEX "idx_battles_series_creator" ON "battles"."series" USING "btree" ("creator_lenser_id");



CREATE INDEX "idx_battles_status_created_at" ON "battles"."battles" USING "btree" ("status", "created_at" DESC, "id" DESC) WHERE ("deleted_at" IS NULL);



CREATE INDEX "idx_battles_task_source" ON "battles"."battles" USING "btree" ("task_source") WHERE ("task_source" IS NOT NULL);



CREATE INDEX "idx_battles_template_id" ON "battles"."battles" USING "btree" ("template_id") WHERE ("template_id" IS NOT NULL);



CREATE INDEX "idx_battles_templates_public_category" ON "battles"."templates" USING "btree" ("category", "created_at" DESC) WHERE (("is_public" = true) AND ("deleted_at" IS NULL));



CREATE INDEX "idx_battles_title_fts" ON "battles"."battles" USING "gin" ("to_tsvector"('"simple"'::"regconfig", "title"));



CREATE INDEX "idx_battles_trending" ON "battles"."battles" USING "btree" ("vote_velocity" DESC, "total_vote_count" DESC) WHERE ("status" = 'published'::"battles"."battle_status_enum");



CREATE INDEX "idx_cla_battle" ON "battles"."contender_lens_assignments" USING "btree" ("battle_id");



CREATE INDEX "idx_cla_lens" ON "battles"."contender_lens_assignments" USING "btree" ("lens_id");



CREATE INDEX "idx_comments_battle_created" ON "battles"."comments" USING "btree" ("battle_id", "created_at");



CREATE INDEX "idx_contender_entity_map_ai_lenser" ON "battles"."contender_entity_map" USING "btree" ("ai_lenser_id") WHERE ("ai_lenser_id" IS NOT NULL);



CREATE INDEX "idx_contender_entity_map_group" ON "battles"."contender_entity_map" USING "btree" ("group_id") WHERE ("group_id" IS NOT NULL);



CREATE INDEX "idx_contender_entity_map_profile" ON "battles"."contender_entity_map" USING "btree" ("profile_id") WHERE ("profile_id" IS NOT NULL);



CREATE INDEX "idx_contender_runs_battle" ON "battles"."contender_runs" USING "btree" ("battle_id");



CREATE INDEX "idx_contender_runs_contender" ON "battles"."contender_runs" USING "btree" ("contender_id");



CREATE INDEX "idx_contender_runs_run" ON "battles"."contender_runs" USING "btree" ("run_id");



CREATE INDEX "idx_contenders_battle" ON "battles"."contenders" USING "btree" ("battle_id");



CREATE INDEX "idx_contenders_battle_status" ON "battles"."contenders" USING "btree" ("battle_id", "contender_status");



CREATE UNIQUE INDEX "idx_contenders_battle_team" ON "battles"."contenders" USING "btree" ("battle_id", "team_id") WHERE ("team_id" IS NOT NULL);



CREATE INDEX "idx_contenders_contender_ref_id" ON "battles"."contenders" USING "btree" ("contender_ref_id");



CREATE INDEX "idx_contenders_ref_id" ON "battles"."contenders" USING "btree" ("contender_ref_id");



CREATE INDEX "idx_cost_ledgers_battle" ON "battles"."cost_ledgers" USING "btree" ("battle_id", "created_at" DESC);



CREATE INDEX "idx_cost_ledgers_contender" ON "battles"."cost_ledgers" USING "btree" ("contender_id") WHERE ("contender_id" IS NOT NULL);



CREATE INDEX "idx_fk_battle_events_actor_id" ON "battles"."events" USING "btree" ("actor_id");



CREATE INDEX "idx_fk_battles_creator_lenser_id" ON "battles"."battles" USING "btree" ("creator_lenser_id");



CREATE INDEX "idx_fk_battles_forum_thread_id" ON "battles"."battles" USING "btree" ("forum_thread_id");



CREATE INDEX "idx_fk_battles_rubric_id" ON "battles"."battles" USING "btree" ("rubric_id");



CREATE INDEX "idx_fk_battles_winner_contender_id" ON "battles"."battles" USING "btree" ("winner_contender_id");



CREATE INDEX "idx_fk_cla_version_id" ON "battles"."contender_lens_assignments" USING "btree" ("version_id");



CREATE INDEX "idx_fk_cost_ledgers_run_id" ON "battles"."cost_ledgers" USING "btree" ("run_id");



CREATE INDEX "idx_fk_funding_policies_sponsor_lenser_id" ON "battles"."funding_policies" USING "btree" ("sponsor_lenser_id");



CREATE INDEX "idx_fk_global_messages_sender_id" ON "battles"."global_messages" USING "btree" ("sender_id");



CREATE INDEX "idx_fk_invitations_invited_by" ON "battles"."invitations" USING "btree" ("invited_by");



CREATE INDEX "idx_fk_invitations_invited_lenser_id" ON "battles"."invitations" USING "btree" ("invited_lenser_id");



CREATE INDEX "idx_fk_rubrics_creator_lenser_id" ON "battles"."rubrics" USING "btree" ("creator_lenser_id");



CREATE INDEX "idx_fk_rule_snapshots_created_by" ON "battles"."rule_snapshots" USING "btree" ("created_by");



CREATE INDEX "idx_fk_scorecards_contender_id" ON "battles"."scorecards" USING "btree" ("contender_id");



CREATE INDEX "idx_fk_scorecards_rubric_criterion_id" ON "battles"."scorecards" USING "btree" ("rubric_criterion_id");



CREATE INDEX "idx_fk_scorecards_scorer_model_id" ON "battles"."scorecards" USING "btree" ("scorer_model_id");



CREATE INDEX "idx_fk_submissions_artifact_id" ON "battles"."submissions" USING "btree" ("artifact_id");



CREATE INDEX "idx_fk_submissions_model_id" ON "battles"."submissions" USING "btree" ("model_id");



CREATE INDEX "idx_fk_submissions_revision_of_id" ON "battles"."submissions" USING "btree" ("revision_of_id");



CREATE INDEX "idx_fk_templates_creator_lenser_id" ON "battles"."templates" USING "btree" ("creator_lenser_id");



CREATE INDEX "idx_fk_templates_rubric_id" ON "battles"."templates" USING "btree" ("rubric_id");



CREATE INDEX "idx_fk_vote_aggregates_contender_id" ON "battles"."vote_aggregates" USING "btree" ("contender_id");



CREATE INDEX "idx_generated_challenges_battle" ON "battles"."generated_challenges" USING "btree" ("battle_id");



CREATE INDEX "idx_global_messages_battle_created" ON "battles"."global_messages" USING "btree" ("battle_id", "created_at");



CREATE INDEX "idx_invitations_battle_status" ON "battles"."invitations" USING "btree" ("battle_id", "status");



CREATE INDEX "idx_invite_links_battle_id" ON "battles"."invite_links" USING "btree" ("battle_id");



CREATE INDEX "idx_invite_links_created_by" ON "battles"."invite_links" USING "btree" ("created_by");



CREATE INDEX "idx_invite_links_token" ON "battles"."invite_links" USING "btree" ("token");



CREATE INDEX "idx_model_test_runs_battle" ON "battles"."model_test_runs" USING "btree" ("battle_id") WHERE ("battle_id" IS NOT NULL);



CREATE INDEX "idx_model_test_runs_model" ON "battles"."model_test_runs" USING "btree" ("model_provider", "model_id", "created_at" DESC);



CREATE INDEX "idx_model_test_runs_template" ON "battles"."model_test_runs" USING "btree" ("template_id") WHERE ("template_id" IS NOT NULL);



CREATE INDEX "idx_schedules_judge_at" ON "battles"."schedules" USING "btree" ("judge_at") WHERE ("judge_at" IS NOT NULL);



CREATE INDEX "idx_schedules_open_at" ON "battles"."schedules" USING "btree" ("open_at") WHERE ("open_at" IS NOT NULL);



CREATE INDEX "idx_schedules_publish_at" ON "battles"."schedules" USING "btree" ("publish_at") WHERE ("publish_at" IS NOT NULL);



CREATE INDEX "idx_scorecards_battle_contender" ON "battles"."scorecards" USING "btree" ("battle_id", "contender_id");



CREATE INDEX "idx_scoring_plugin_signals_battle" ON "battles"."scoring_plugin_signals" USING "btree" ("battle_id", "recorded_at" DESC);



CREATE INDEX "idx_series_rounds_battle" ON "battles"."series_rounds" USING "btree" ("battle_id");



CREATE INDEX "idx_sponsorship_contribs_lenser" ON "battles"."sponsorship_contributions" USING "btree" ("contributor_lenser_id", "created_at" DESC);



CREATE INDEX "idx_sponsorship_contribs_pool" ON "battles"."sponsorship_contributions" USING "btree" ("pool_id");



CREATE INDEX "idx_sponsorship_payouts_pool" ON "battles"."sponsorship_payouts" USING "btree" ("pool_id");



CREATE INDEX "idx_sponsorship_payouts_recipient" ON "battles"."sponsorship_payouts" USING "btree" ("recipient_lenser_id", "created_at" DESC);



CREATE INDEX "idx_sponsorship_pools_battle" ON "battles"."sponsorship_pools" USING "btree" ("battle_id");



CREATE INDEX "idx_sponsorship_pools_status" ON "battles"."sponsorship_pools" USING "btree" ("status", "created_at" DESC) WHERE ("status" <> ALL (ARRAY['distributed'::"battles"."sponsorship_pool_status_enum", 'refunded'::"battles"."sponsorship_pool_status_enum"]));



CREATE UNIQUE INDEX "idx_submissions_battle_contender_final" ON "battles"."submissions" USING "btree" ("battle_id", "contender_id") WHERE ("is_final" = true);



CREATE INDEX "idx_submissions_battle_status" ON "battles"."submissions" USING "btree" ("battle_id", "status");



COMMENT ON INDEX "battles"."idx_submissions_battle_status" IS 'Supports submission listing per battle filtered by status. battle_id is the equality filter; status is the secondary discriminator.';



CREATE INDEX "idx_submissions_contender_time" ON "battles"."submissions" USING "btree" ("contender_id", "submitted_at" DESC);



CREATE UNIQUE INDEX "idx_submissions_execution_run" ON "battles"."submissions" USING "btree" ("execution_run_id") WHERE ("execution_run_id" IS NOT NULL);



CREATE INDEX "idx_submissions_workflow" ON "battles"."submissions" USING "btree" ("workflow_id") WHERE ("workflow_id" IS NOT NULL);



CREATE INDEX "idx_template_prompt_variables_template" ON "battles"."template_prompt_variables" USING "btree" ("template_id", "ordinal");



CREATE INDEX "idx_templates_recurrence_due" ON "battles"."templates" USING "btree" ("next_run_at") WHERE (("recurrence_rule" IS NOT NULL) AND ("next_run_at" IS NOT NULL));



CREATE INDEX "idx_tournament_matches_battle" ON "battles"."tournament_matches" USING "btree" ("battle_id") WHERE ("battle_id" IS NOT NULL);



CREATE INDEX "idx_vote_aggregates_rank" ON "battles"."vote_aggregates" USING "btree" ("battle_id", "rank_position");



CREATE INDEX "idx_vote_aggregates_score" ON "battles"."vote_aggregates" USING "btree" ("battle_id", "weighted_vote_sum" DESC);



CREATE INDEX "idx_vote_choices_contender" ON "battles"."vote_choices" USING "btree" ("contender_id");



CREATE INDEX "idx_vote_choices_rank" ON "battles"."vote_choices" USING "btree" ("vote_id", "rank_ordinal") WHERE ("rank_ordinal" IS NOT NULL);



CREATE UNIQUE INDEX "idx_vote_choices_vote_contender" ON "battles"."vote_choices" USING "btree" ("vote_id", "contender_id");



CREATE INDEX "idx_votes_battle_created" ON "battles"."votes" USING "btree" ("battle_id", "created_at" DESC);



CREATE INDEX "idx_votes_battle_voter" ON "battles"."votes" USING "btree" ("battle_id", "voter_lenser_id");



COMMENT ON INDEX "battles"."idx_votes_battle_voter" IS 'Fast duplicate-vote check: WHERE battle_id = X AND voter_lenser_id = Y. Plain index (not UNIQUE) to allow multi-contender ranked voting patterns.';



CREATE INDEX "idx_votes_contender" ON "battles"."votes" USING "btree" ("voted_contender_id") WHERE ("voted_contender_id" IS NOT NULL);



CREATE INDEX "idx_votes_velocity" ON "battles"."votes" USING "btree" ("voter_lenser_id", "created_at" DESC);



CREATE INDEX "idx_votes_voter_lenser" ON "battles"."votes" USING "btree" ("voter_lenser_id", "created_at" DESC);



CREATE UNIQUE INDEX "uq_generated_challenges_locked_per_battle" ON "battles"."generated_challenges" USING "btree" ("battle_id") WHERE ("status" = 'locked'::"text");



CREATE UNIQUE INDEX "uq_sponsorship_contribs_pool_tx" ON "battles"."sponsorship_contributions" USING "btree" ("pool_id", "external_tx_ref") WHERE ("external_tx_ref" IS NOT NULL);



CREATE UNIQUE INDEX "uq_sponsorship_payouts_pool_tx" ON "battles"."sponsorship_payouts" USING "btree" ("pool_id", "external_tx_ref") WHERE ("external_tx_ref" IS NOT NULL);



CREATE INDEX "idx_billing_checkout_active" ON "billing"."checkout_sessions" USING "btree" ("status") WHERE ("status" = 'created'::"text");



CREATE INDEX "idx_billing_checkout_lenser" ON "billing"."checkout_sessions" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_billing_execution_margin_policies_active" ON "billing"."execution_margin_policies" USING "btree" ("model_id", "effective_from" DESC) WHERE (("is_active" = true) AND ("effective_to" IS NULL));



CREATE INDEX "idx_billing_margin_policies_active" ON "billing"."execution_margin_policies" USING "btree" ("model_id", "is_active") WHERE (("is_active" = true) AND ("effective_to" IS NULL));



CREATE INDEX "idx_billing_orders_lenser" ON "billing"."orders" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_billing_orders_ls_id" ON "billing"."orders" USING "btree" ("ls_order_id");



CREATE INDEX "idx_billing_orders_product_paid" ON "billing"."orders" USING "btree" ("product_id", "status") WHERE ("status" = 'paid'::"billing"."order_status_enum");



CREATE INDEX "idx_billing_orders_status" ON "billing"."orders" USING "btree" ("status", "created_at" DESC);



CREATE INDEX "idx_billing_orders_webhook" ON "billing"."orders" USING "btree" ("webhook_event_id");



CREATE INDEX "idx_billing_products_ls_id" ON "billing"."products" USING "btree" ("ls_product_id");



CREATE INDEX "idx_billing_products_status" ON "billing"."products" USING "btree" ("status") WHERE ("status" = 'published'::"billing"."product_status_enum");



CREATE INDEX "idx_billing_variants_ls_id" ON "billing"."variants" USING "btree" ("ls_variant_id");



CREATE INDEX "idx_billing_variants_product" ON "billing"."variants" USING "btree" ("product_id");



CREATE INDEX "idx_billing_webhook_event_name" ON "billing"."webhook_events" USING "btree" ("event_name", "created_at" DESC);



CREATE INDEX "idx_billing_webhook_unprocessed" ON "billing"."webhook_events" USING "btree" ("processed", "created_at") WHERE ("processed" = false);



CREATE INDEX "idx_cost_reservations_held_until" ON "billing"."cost_reservations" USING "btree" ("held_until") WHERE ("status" = 'held'::"text");



CREATE INDEX "idx_cost_reservations_lenser_created" ON "billing"."cost_reservations" USING "btree" ("ai_lenser_id", "created_at" DESC);



CREATE INDEX "idx_cost_reservations_lenser_model_status" ON "billing"."cost_reservations" USING "btree" ("ai_lenser_id", "model_id", "status");



CREATE INDEX "idx_cost_reservations_lenser_status_created" ON "billing"."cost_reservations" USING "btree" ("ai_lenser_id", "status", "created_at");



CREATE INDEX "idx_fk_checkout_sessions_variant_id" ON "billing"."checkout_sessions" USING "btree" ("variant_id");



CREATE INDEX "idx_fk_orders_variant_id" ON "billing"."orders" USING "btree" ("variant_id");



CREATE INDEX "idx_ledger_entries_lenser_recorded" ON "billing"."ledger_entries" USING "btree" ("ai_lenser_id", "recorded_at" DESC);



CREATE INDEX "idx_ledger_entries_reservation" ON "billing"."ledger_entries" USING "btree" ("reservation_id", "id");



CREATE INDEX "idx_pricing_snapshots_model_taken" ON "billing"."pricing_snapshots" USING "btree" ("model_id", "taken_at" DESC);



CREATE INDEX "idx_spend_anomalies_lenser_created" ON "billing"."spend_anomalies" USING "btree" ("ai_lenser_id", "created_at" DESC);



CREATE INDEX "connector_tokens_connector_active_idx" ON "connectors"."connector_tokens" USING "btree" ("connector_id") WHERE ("revoked_at" IS NULL);



CREATE INDEX "connectors_workspace_active_idx" ON "connectors"."connectors" USING "btree" ("workspace_id") WHERE ("is_active" = true);



CREATE INDEX "idx_entity_translations_entity" ON "content"."entity_translations" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_entity_translations_original" ON "content"."entity_translations" USING "btree" ("entity_type", "entity_id") WHERE ("is_original" = true);



CREATE INDEX "idx_entity_translations_title_trgm" ON "content"."entity_translations" USING "gin" ("lower"("title") "extensions"."gin_trgm_ops") WHERE (("is_original" = true) AND ("title" IS NOT NULL));



CREATE INDEX "idx_fk_entity_translations_language_code" ON "content"."entity_translations" USING "btree" ("language_code");



CREATE INDEX "idx_fk_tag_suggestions_ai_model_id" ON "content"."tag_suggestions" USING "btree" ("ai_model_id");



CREATE INDEX "idx_fk_tag_suggestions_tag_id" ON "content"."tag_suggestions" USING "btree" ("tag_id");



CREATE INDEX "idx_fk_tag_translations_language_code" ON "content"."tag_translations" USING "btree" ("language_code");



CREATE INDEX "idx_fk_thread_replies_lenser_id" ON "content"."thread_replies" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_thread_replies_parent_reply_id" ON "content"."thread_replies" USING "btree" ("parent_reply_id");



CREATE INDEX "idx_fk_threads_linked_lens_id" ON "content"."threads" USING "btree" ("linked_lens_id");



CREATE INDEX "idx_reactions_entity" ON "content"."reactions" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_reactions_entity_reaction" ON "content"."reactions" USING "btree" ("entity_type", "entity_id", "reaction");



CREATE INDEX "idx_reactions_lenser_time" ON "content"."reactions" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_reports_target_type_target_id" ON "content"."reports" USING "btree" ("target_type", "target_id");



CREATE INDEX "idx_tag_map_entity" ON "content"."tag_map" USING "btree" ("entity_id", "entity_type");



CREATE INDEX "idx_tag_map_tag_id" ON "content"."tag_map" USING "btree" ("tag_id");



CREATE INDEX "idx_tag_map_type_entity" ON "content"."tag_map" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_tag_map_type_tag" ON "content"."tag_map" USING "btree" ("entity_type", "tag_id", "entity_id");



CREATE INDEX "idx_tag_suggestions_entity" ON "content"."tag_suggestions" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_tag_translations_name_lower" ON "content"."tag_translations" USING "btree" ("lower"("btrim"("name")));



CREATE INDEX "idx_tag_translations_tag_id" ON "content"."tag_translations" USING "btree" ("tag_id");



CREATE INDEX "idx_tags_slug" ON "content"."tags" USING "btree" ("slug");



CREATE INDEX "idx_thread_replies_published" ON "content"."thread_replies" USING "btree" ("thread_id", "created_at") WHERE (("status" = 'published'::"content"."thread_reply_status") AND ("deleted_at" IS NULL));



CREATE INDEX "idx_threads_lenser_id" ON "content"."threads" USING "btree" ("lenser_id");



CREATE INDEX "idx_threads_lenser_id_created" ON "content"."threads" USING "btree" ("lenser_id", "created_at" DESC) WHERE (("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status"));



CREATE INDEX "idx_threads_public_feed" ON "content"."threads" USING "btree" ("visibility", "status", "created_at" DESC) WHERE (("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status"));



CREATE INDEX "idx_threads_public_published" ON "content"."threads" USING "btree" ("created_at" DESC) WHERE (("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status"));



CREATE UNIQUE INDEX "reactions_unique_non_copy" ON "content"."reactions" USING "btree" ("entity_type", "entity_id", "lenser_id", "reaction") WHERE ("reaction" <> 'copy'::"content"."reaction_enum");



CREATE INDEX "idx_cities_country_id" ON "core"."cities" USING "btree" ("country_id");



CREATE INDEX "idx_cities_state_id" ON "core"."cities" USING "btree" ("state_id");



CREATE INDEX "idx_countries_region_id" ON "core"."countries" USING "btree" ("region_id");



CREATE INDEX "idx_countries_subregion_id" ON "core"."countries" USING "btree" ("subregion_id");



CREATE INDEX "idx_states_country_id" ON "core"."states" USING "btree" ("country_id");



CREATE INDEX "idx_subregions_region_id" ON "core"."subregions" USING "btree" ("region_id");



CREATE INDEX "idx_device_challenges_device" ON "devices"."device_challenges" USING "btree" ("device_id");



CREATE INDEX "idx_device_challenges_status" ON "devices"."device_challenges" USING "btree" ("status");



CREATE INDEX "idx_devices_last_heartbeat" ON "devices"."registered_devices" USING "btree" ("last_heartbeat_at" DESC NULLS LAST);



CREATE INDEX "idx_devices_last_seen" ON "devices"."registered_devices" USING "btree" ("last_seen_at" DESC NULLS LAST);



CREATE INDEX "idx_devices_lenser_id" ON "devices"."registered_devices" USING "btree" ("lenser_id");



CREATE INDEX "idx_devices_trust_level" ON "devices"."registered_devices" USING "btree" ("trust_level") WHERE ("trust_level" <> ALL (ARRAY['revoked'::"text", 'blocked'::"text"]));



CREATE INDEX "idx_nonce_cache_expires_at" ON "devices"."nonce_cache" USING "btree" ("expires_at");



CREATE INDEX "idx_peer_leases_device" ON "devices"."peer_leases" USING "btree" ("device_id");



CREATE INDEX "idx_sync_outbox_class" ON "devices"."sync_outbox" USING "btree" ("object_class", "applied_at");



CREATE INDEX "idx_sync_outbox_device" ON "devices"."sync_outbox" USING "btree" ("device_id", "created_at");



CREATE INDEX "idx_sync_outbox_lenser" ON "devices"."sync_outbox" USING "btree" ("lenser_id", "created_at");



CREATE INDEX "idx_sync_outbox_unapplied" ON "devices"."sync_outbox" USING "btree" ("lenser_id", "created_at") WHERE ("applied_at" IS NULL);



CREATE INDEX "idx_sync_watermarks_lenser" ON "devices"."sync_watermarks" USING "btree" ("lenser_id");



CREATE INDEX "idx_artifact_medias_artifact_id" ON "execution"."artifact_medias" USING "btree" ("artifact_id");



CREATE INDEX "idx_artifact_medias_media_id" ON "execution"."artifact_medias" USING "btree" ("media_id");



CREATE INDEX "idx_artifacts_run_visibility" ON "execution"."artifacts" USING "btree" ("run_id", "visibility") WHERE ("visibility" <> 'private'::"text");



CREATE INDEX "idx_att_verifications_verified" ON "execution"."attestation_verifications" USING "btree" ("verified");



CREATE INDEX "idx_byok_keys_agent" ON "execution"."byok_keys" USING "btree" ("agent_id") WHERE ("revoked_at" IS NULL);



CREATE INDEX "idx_exec_artifacts_kind" ON "execution"."artifacts" USING "btree" ("artifact_kind", "created_at" DESC);



CREATE INDEX "idx_exec_artifacts_run_primary" ON "execution"."artifacts" USING "btree" ("run_id", "is_primary_output" DESC);



CREATE INDEX "idx_exec_attestations_device_id" ON "execution"."attestations" USING "btree" ("device_id") WHERE ("device_id" IS NOT NULL);



CREATE INDEX "idx_exec_attestations_envelope_kid" ON "execution"."attestations" USING "btree" ("envelope_kid") WHERE ("envelope_kid" IS NOT NULL);



CREATE INDEX "idx_exec_attestations_run_id" ON "execution"."attestations" USING "btree" ("run_id");



CREATE INDEX "idx_exec_requests_byok_key" ON "execution"."requests" USING "btree" ("byok_key_ref_id") WHERE ("byok_key_ref_id" IS NOT NULL);



CREATE INDEX "idx_exec_requests_origin" ON "execution"."requests" USING "btree" ("origin_type", "origin_id");



CREATE INDEX "idx_exec_requests_requester_origin" ON "execution"."requests" USING "btree" ("requester_lenser_id", "origin_type", "created_at" DESC);



CREATE INDEX "idx_exec_runs_billing_status" ON "execution"."runs" USING "btree" ("billing_status", "created_at" DESC) WHERE ("billing_status" <> 'free'::"text");



CREATE UNIQUE INDEX "idx_exec_runs_execution_hash" ON "execution"."runs" USING "btree" ("execution_hash") WHERE ("execution_hash" IS NOT NULL);



CREATE INDEX "idx_exec_runs_model" ON "execution"."runs" USING "btree" ("model_id", "started_at" DESC) WHERE ("model_id" IS NOT NULL);



CREATE UNIQUE INDEX "idx_exec_runs_provider_request" ON "execution"."runs" USING "btree" ("provider_request_id") WHERE ("provider_request_id" IS NOT NULL);



CREATE INDEX "idx_exec_runs_request" ON "execution"."runs" USING "btree" ("request_id");



CREATE INDEX "idx_exec_runs_request_status" ON "execution"."runs" USING "btree" ("request_id", "status");



CREATE INDEX "idx_exec_runs_status" ON "execution"."runs" USING "btree" ("status", "started_at" DESC);



CREATE INDEX "idx_exec_runs_status_billing" ON "execution"."runs" USING "btree" ("status", "billing_status") WHERE ("billing_status" <> 'free'::"text");



COMMENT ON INDEX "execution"."idx_exec_runs_status_billing" IS 'Billing reconciliation: succeeded runs awaiting billing_status=pending. Partial index excludes free runs (largest segment) to minimize index size.';



CREATE UNIQUE INDEX "idx_exec_steps_run_ordinal" ON "execution"."steps" USING "btree" ("run_id", "ordinal");



CREATE INDEX "idx_exec_steps_run_type" ON "execution"."steps" USING "btree" ("run_id", "step_type");



CREATE INDEX "idx_execution_links_entity" ON "execution"."links" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_execution_links_run_id" ON "execution"."links" USING "btree" ("run_id");



CREATE INDEX "idx_execution_requests_idempotency" ON "execution"."requests" USING "btree" ("requester_lenser_id", "idempotency_key") WHERE (("idempotency_key" IS NOT NULL) AND ("is_active" = true));



CREATE INDEX "idx_execution_requests_is_active" ON "execution"."requests" USING "btree" ("is_active", "created_at" DESC) WHERE ("is_active" = true);



CREATE INDEX "idx_execution_requests_lens_snapshot" ON "execution"."requests" USING "btree" ("lens_id", "version_id", "resolved_artifact_snapshot_hash");



CREATE INDEX "idx_execution_runs_async_pending" ON "execution"."runs" USING "btree" ("is_async", "status", "started_at") WHERE (("is_async" = true) AND ("status" = 'running'::"text"));



CREATE INDEX "idx_execution_runs_async_poll_order" ON "execution"."runs" USING "btree" ("is_async", "status", "last_polled_at" NULLS FIRST) WHERE (("is_async" = true) AND ("status" = 'running'::"text"));



CREATE INDEX "idx_execution_runs_is_active" ON "execution"."runs" USING "btree" ("is_active", "created_at" DESC) WHERE ("is_active" = true);



CREATE INDEX "idx_execution_steps_definition_id" ON "execution"."steps" USING "btree" ("step_definition_id") WHERE ("step_definition_id" IS NOT NULL);



CREATE INDEX "idx_execution_tags_run_id" ON "execution"."execution_tags" USING "btree" ("run_id");



CREATE INDEX "idx_execution_tags_tag_severity" ON "execution"."execution_tags" USING "btree" ("tag", "severity", "created_at" DESC);



CREATE INDEX "idx_fk_param_usage_logs_ai_model_id" ON "execution"."parameter_usage_logs" USING "btree" ("ai_model_id");



CREATE INDEX "idx_fk_param_usage_logs_version_parameter_id" ON "execution"."parameter_usage_logs" USING "btree" ("version_parameter_id");



CREATE INDEX "idx_fk_param_usage_logs_workspace_id" ON "execution"."parameter_usage_logs" USING "btree" ("workspace_id");



CREATE INDEX "idx_fk_requests_model_id" ON "execution"."requests" USING "btree" ("model_id");



CREATE INDEX "idx_fk_stream_sessions_model_key" ON "execution"."stream_sessions" USING "btree" ("model_key");



CREATE INDEX "idx_fk_stream_sessions_provider" ON "execution"."stream_sessions" USING "btree" ("provider");



CREATE INDEX "idx_param_usage_logs_lenser_id" ON "execution"."parameter_usage_logs" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_param_usage_logs_request_id" ON "execution"."parameter_usage_logs" USING "btree" ("request_id");



CREATE INDEX "idx_param_usage_logs_version_id" ON "execution"."parameter_usage_logs" USING "btree" ("version_id") WHERE ("version_id" IS NOT NULL);



CREATE INDEX "idx_rdb_device_id" ON "execution"."runner_device_bindings" USING "btree" ("device_id");



CREATE INDEX "idx_rdb_runner_id" ON "execution"."runner_device_bindings" USING "btree" ("runner_id");



CREATE INDEX "idx_req_attachments_media_object" ON "execution"."request_attachments" USING "btree" ("media_object_id");



CREATE INDEX "idx_req_attachments_request_id" ON "execution"."request_attachments" USING "btree" ("request_id");



CREATE INDEX "idx_requests_lens_id" ON "execution"."requests" USING "btree" ("lens_id") WHERE ("is_active" = true);



CREATE INDEX "idx_requests_lenser_created" ON "execution"."requests" USING "btree" ("requester_lenser_id", "created_at" DESC) WHERE ("is_active" = true);



CREATE INDEX "idx_requests_requester_time" ON "execution"."requests" USING "btree" ("requester_lenser_id", "created_at" DESC);



CREATE INDEX "idx_requests_version_id" ON "execution"."requests" USING "btree" ("version_id") WHERE ("version_id" IS NOT NULL);



CREATE INDEX "idx_requests_workspace_created" ON "execution"."requests" USING "btree" ("workspace_id", "created_at" DESC) WHERE ("workspace_id" IS NOT NULL);



CREATE INDEX "idx_requests_workspace_id" ON "execution"."requests" USING "btree" ("workspace_id");



CREATE INDEX "idx_runners_active" ON "execution"."runners" USING "btree" ("is_active") WHERE ("is_active" = true);



CREATE INDEX "idx_runners_lenser_id" ON "execution"."runners" USING "btree" ("lenser_id");



CREATE INDEX "idx_runs_request_id" ON "execution"."runs" USING "btree" ("request_id") WHERE ("is_active" = true);



CREATE INDEX "idx_stream_sessions_lenser_id" ON "execution"."stream_sessions" USING "btree" ("lenser_id", "started_at" DESC);



CREATE INDEX "idx_stream_sessions_status_started" ON "execution"."stream_sessions" USING "btree" ("status", "started_at" DESC);



COMMENT ON INDEX "execution"."idx_stream_sessions_status_started" IS 'Cross-user session monitoring by status (open, failed, timed_out). Also used by cleanup jobs sweeping stale open sessions.';



CREATE INDEX "idx_stream_sessions_workspace" ON "execution"."stream_sessions" USING "btree" ("workspace_id", "started_at" DESC) WHERE ("workspace_id" IS NOT NULL);



CREATE INDEX "idx_trust_evals_submission_id" ON "execution"."trust_evaluations" USING "btree" ("submission_id");



CREATE INDEX "idx_trust_evals_trust_level" ON "execution"."trust_evaluations" USING "btree" ("trust_level");



CREATE INDEX "reserved_namespaces_exact_idx" ON "identity_gov"."reserved_namespaces" USING "btree" ("entry_kind", "value") WHERE ("entry_kind" = 'exact'::"text");



CREATE INDEX "reserved_namespaces_exact_len_idx" ON "identity_gov"."reserved_namespaces" USING "btree" ("length"("value")) WHERE ("entry_kind" = 'exact'::"text");



CREATE INDEX "reserved_namespaces_expires_idx" ON "identity_gov"."reserved_namespaces" USING "btree" ("expires_at") WHERE ("expires_at" IS NOT NULL);



CREATE INDEX "reserved_namespaces_kind_idx" ON "identity_gov"."reserved_namespaces" USING "btree" ("entry_kind");



CREATE INDEX "reserved_namespaces_metaphone_idx" ON "identity_gov"."reserved_namespaces" USING "btree" ("extensions"."dmetaphone"("value")) WHERE ("entry_kind" = 'exact'::"text");



CREATE INDEX "reserved_namespaces_skeleton_idx" ON "identity_gov"."reserved_namespaces" USING "btree" ("identity_gov"."fn_handle_skeleton"("value")) WHERE ("entry_kind" = 'exact'::"text");



CREATE INDEX "reserved_namespaces_trgm_idx" ON "identity_gov"."reserved_namespaces" USING "gin" ("value" "extensions"."gin_trgm_ops") WHERE ("entry_kind" = 'exact'::"text");



CREATE INDEX "idx_fk_tool_allowlist_granted_by" ON "integrations"."tool_allowlist" USING "btree" ("granted_by");



CREATE INDEX "idx_fk_tool_allowlist_tool_id" ON "integrations"."tool_allowlist" USING "btree" ("tool_id");



CREATE INDEX "idx_fk_preferences_active_lenser_id" ON "lensers"."preferences" USING "btree" ("active_lenser_id");



CREATE INDEX "idx_fk_preferences_ai_model_key" ON "lensers"."preferences" USING "btree" ("ai_model_key");



CREATE INDEX "idx_fk_preferences_currency" ON "lensers"."preferences" USING "btree" ("currency");



CREATE INDEX "idx_fk_profiles_ai_model_id" ON "lensers"."profiles" USING "btree" ("ai_model_id");



CREATE INDEX "idx_group_members_lenser" ON "lensers"."group_members" USING "btree" ("lenser_id");



COMMENT ON INDEX "lensers"."idx_group_members_lenser" IS 'Supports "groups I belong to" queries starting from a lenser_id. The PK index covers (group_id, lenser_id) lookups.';



CREATE INDEX "idx_groups_owner" ON "lensers"."groups" USING "btree" ("owner_lenser_id");



COMMENT ON INDEX "lensers"."idx_groups_owner" IS 'Supports "my groups" listing filtered by owner_lenser_id.';



CREATE INDEX "idx_groups_type_visibility" ON "lensers"."groups" USING "btree" ("type", "visibility");



COMMENT ON INDEX "lensers"."idx_groups_type_visibility" IS 'Supports discovery queries: list all public communities, list all private teams, etc.';



CREATE INDEX "idx_lensers_profiles_type" ON "lensers"."profiles" USING "btree" ("type");



CREATE INDEX "idx_lensers_social_links_lenser_id" ON "lensers"."social_links" USING "btree" ("lenser_id");



CREATE INDEX "idx_preferences_ai_provider_key" ON "lensers"."preferences" USING "btree" ("ai_provider_key") WHERE ("ai_provider_key" IS NOT NULL);



COMMENT ON INDEX "lensers"."idx_preferences_ai_provider_key" IS 'Partial index for filtering preferences by ai_provider_key — used in billing/analytics dashboards.';



CREATE INDEX "idx_preferences_language" ON "lensers"."preferences" USING "btree" ("language");



COMMENT ON INDEX "lensers"."idx_preferences_language" IS 'Index on language for language-based analytics and i18n dashboard filtering.';



CREATE INDEX "idx_preferences_selected_api_key_id" ON "lensers"."preferences" USING "btree" ("selected_api_key_id") WHERE ("selected_api_key_id" IS NOT NULL);



COMMENT ON INDEX "lensers"."idx_preferences_selected_api_key_id" IS 'Partial index for looking up which lensers use a specific API key.';



CREATE INDEX "idx_preferences_wallet_mode" ON "lensers"."preferences" USING "btree" ("wallet_mode");



COMMENT ON INDEX "lensers"."idx_preferences_wallet_mode" IS 'Index on wallet_mode for BYOK vs CLOUD usage filtering.';



CREATE INDEX "idx_profiles_handle" ON "lensers"."profiles" USING "btree" ("handle");



CREATE INDEX "idx_profiles_joined_at" ON "lensers"."profiles" USING "btree" ("created_at" DESC);



CREATE INDEX "idx_profiles_last_active_at" ON "lensers"."profiles" USING "btree" ("last_active_at" DESC NULLS LAST) WHERE (("status" = 'active'::"lensers"."lenser_status") AND ("visibility" = 'public'::"lensers"."lenser_visibility") AND ("deletion_requested_at" IS NULL));



CREATE INDEX "idx_profiles_user_id_status" ON "lensers"."profiles" USING "btree" ("user_id", "status") WHERE ("deletion_requested_at" IS NULL);



CREATE INDEX "idx_profiles_user_id_status_all" ON "lensers"."profiles" USING "btree" ("user_id", "status");



CREATE INDEX "idx_relationships_accepted_active" ON "lensers"."relationships" USING "btree" ("source_profile_id", "target_profile_id") WHERE (("status" = 'accepted'::"lensers"."relationship_status") AND ("removed_at" IS NULL));



COMMENT ON INDEX "lensers"."idx_relationships_accepted_active" IS 'Partial index on accepted, non-removed follows. Used by fn_content_get_following_threads and fn_content_get_following_lenses for fast fan-out lookup.';



CREATE INDEX "idx_relationships_active" ON "lensers"."relationships" USING "btree" ("source_profile_id", "target_profile_id") WHERE ("status" = ANY (ARRAY['pending'::"lensers"."relationship_status", 'accepted'::"lensers"."relationship_status"]));



CREATE INDEX "idx_relationships_blocked" ON "lensers"."relationships" USING "btree" ("source_profile_id", "target_profile_id") WHERE ("status" = 'blocked'::"lensers"."relationship_status");



CREATE INDEX "idx_relationships_source_status" ON "lensers"."relationships" USING "btree" ("source_profile_id", "status");



CREATE INDEX "idx_relationships_target" ON "lensers"."relationships" USING "btree" ("target_profile_id", "status") WHERE ("removed_at" IS NULL);



CREATE INDEX "idx_relationships_target_status" ON "lensers"."relationships" USING "btree" ("target_profile_id", "status");



CREATE INDEX "idx_relationships_uq_active_source" ON "lensers"."relationships" USING "btree" ("source_profile_id", "status") WHERE ("removed_at" IS NULL);



COMMENT ON INDEX "lensers"."idx_relationships_uq_active_source" IS 'Partial index for active (non-removed) follow relationships by source lenser. Added in migration 000056.';



CREATE INDEX "idx_tag_follows_lenser_id" ON "lensers"."tag_follows" USING "btree" ("lenser_id");



CREATE INDEX "idx_tag_follows_tag_id" ON "lensers"."tag_follows" USING "btree" ("tag_id");



CREATE UNIQUE INDEX "lensers_handle_lower_idx" ON "lensers"."profiles" USING "btree" ("lower"("handle"));



CREATE INDEX "lensers_profiles_active_recent_idx" ON "lensers"."profiles" USING "btree" ("created_at" DESC) WHERE (("status" = 'active'::"lensers"."lenser_status") AND ("deletion_requested_at" IS NULL));



CREATE INDEX "capability_index_fts_idx" ON "lenses"."capability_index" USING "gin" ("fts_doc");



CREATE INDEX "capability_index_kind_idx" ON "lenses"."capability_index" USING "btree" ("lens_kind");



CREATE INDEX "capability_index_tags_idx" ON "lenses"."capability_index" USING "gin" ("capability_tags");



CREATE INDEX "contract_channels_hash_idx" ON "lenses"."contract_channels" USING "btree" ("content_hash");



CREATE INDEX "contracts_lens_published_idx" ON "lenses"."contracts" USING "btree" ("lens_id", "published_at" DESC);



CREATE INDEX "contracts_lens_semver_idx" ON "lenses"."contracts" USING "btree" ("lens_id", "semver");



CREATE INDEX "contracts_supersedes_idx" ON "lenses"."contracts" USING "btree" ("supersedes_hash") WHERE ("supersedes_hash" IS NOT NULL);



CREATE INDEX "contracts_version_idx" ON "lenses"."contracts" USING "btree" ("version_id");



CREATE INDEX "dependency_edges_child_idx" ON "lenses"."dependency_edges" USING "btree" ("child_content_hash");



CREATE INDEX "dependency_edges_parent_idx" ON "lenses"."dependency_edges" USING "btree" ("parent_content_hash");



CREATE INDEX "execution_records_content_hash_idx" ON "lenses"."execution_records" USING "btree" ("content_hash", "compiled_at" DESC);



CREATE INDEX "execution_records_idempotency_idx" ON "lenses"."execution_records" USING "btree" ("idempotency_key") WHERE ("idempotency_key" IS NOT NULL);



CREATE INDEX "execution_records_principal_idx" ON "lenses"."execution_records" USING "btree" ("principal_lenser_id", "compiled_at" DESC) WHERE ("principal_lenser_id" IS NOT NULL);



CREATE INDEX "execution_records_trace_idx" ON "lenses"."execution_records" USING "btree" ("trace_id");



CREATE INDEX "idx_fk_lenses_forked_from_execution_id" ON "lenses"."lenses" USING "btree" ("forked_from_execution_id");



CREATE INDEX "idx_fk_lenses_forked_from_version_id" ON "lenses"."lenses" USING "btree" ("forked_from_version_id");



CREATE INDEX "idx_fk_steps_model_id" ON "lenses"."steps" USING "btree" ("model_id");



CREATE INDEX "idx_fk_vpc_workspace_id" ON "lenses"."version_parameter_contents" USING "btree" ("workspace_id");



CREATE INDEX "idx_fk_wf_nodes_lens_id" ON "lenses"."workflow_nodes" USING "btree" ("lens_id");



CREATE INDEX "idx_fk_wf_nodes_version_id" ON "lenses"."workflow_nodes" USING "btree" ("version_id");



CREATE INDEX "idx_fk_wnr_execution_run_id" ON "lenses"."workflow_node_results" USING "btree" ("execution_run_id");



CREATE INDEX "idx_fk_wnr_node_id" ON "lenses"."workflow_node_results" USING "btree" ("node_id");



CREATE INDEX "idx_lens_steps_lens_id" ON "lenses"."steps" USING "btree" ("lens_id");



CREATE INDEX "idx_lens_steps_version_id" ON "lenses"."steps" USING "btree" ("version_id");



CREATE INDEX "idx_lens_version_params_tool_id" ON "lenses"."version_parameters" USING "btree" ("tool_id") WHERE ("tool_id" IS NOT NULL);



CREATE INDEX "idx_lens_versions_draft" ON "lenses"."versions" USING "btree" ("lens_id", "version_number" DESC) WHERE ("status" = 'draft'::"content"."content_status");



CREATE INDEX "idx_lens_versions_lens_number" ON "lenses"."versions" USING "btree" ("lens_id", "version_number" DESC);



CREATE INDEX "idx_lens_versions_lens_status_covering" ON "lenses"."versions" USING "btree" ("lens_id", "status") INCLUDE ("id", "version_number", "published_at");



CREATE INDEX "idx_lens_versions_parent" ON "lenses"."versions" USING "btree" ("parent_version_id") WHERE ("parent_version_id" IS NOT NULL);



CREATE INDEX "idx_lens_versions_published_covering" ON "lenses"."versions" USING "btree" ("lens_id", "version_number" DESC) INCLUDE ("id", "published_at") WHERE ("status" = 'published'::"content"."content_status");



CREATE INDEX "idx_lenses_featured" ON "lenses"."lenses" USING "btree" ("is_featured") WHERE ("is_featured" = true);



CREATE INDEX "idx_lenses_head_version_id" ON "lenses"."lenses" USING "btree" ("head_version_id");



CREATE INDEX "idx_lenses_lenser_id" ON "lenses"."lenses" USING "btree" ("lenser_id");



CREATE INDEX "idx_lenses_lenser_id_created" ON "lenses"."lenses" USING "btree" ("lenser_id", "created_at" DESC) WHERE (("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status"));



CREATE INDEX "idx_lenses_lifecycle_owner" ON "lenses"."lenses" USING "btree" ("lenser_id", "status", "visibility", "archived_at", "deleted_at");



CREATE INDEX "idx_lenses_parent_lens_id" ON "lenses"."lenses" USING "btree" ("parent_lens_id") WHERE ("parent_lens_id" IS NOT NULL);



CREATE INDEX "idx_lenses_public_created" ON "lenses"."lenses" USING "btree" ("created_at" DESC) WHERE (("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status"));



CREATE INDEX "idx_lenses_public_feed" ON "lenses"."lenses" USING "btree" ("visibility", "status", "created_at" DESC) WHERE (("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status"));



CREATE INDEX "idx_lenses_tools_category" ON "lenses"."tools" USING "btree" ("category", "sort_order");



CREATE INDEX "idx_lenses_tools_is_system" ON "lenses"."tools" USING "btree" ("is_system") WHERE ("is_system" = true);



CREATE INDEX "idx_lenses_visibility_status" ON "lenses"."lenses" USING "btree" ("visibility", "status");



COMMENT ON INDEX "lenses"."idx_lenses_visibility_status" IS 'Supports discovery queries: list all public published prompts, filter by visibility + status combinations.';



CREATE INDEX "idx_schedule_calendars_lenser" ON "lenses"."schedule_calendars" USING "btree" ("lenser_id");



CREATE INDEX "idx_schedule_calendars_seed" ON "lenses"."schedule_calendars" USING "btree" ("is_seed") WHERE ("is_seed" = true);



CREATE INDEX "idx_steps_sub_lens_id" ON "lenses"."steps" USING "btree" ("sub_lens_id") WHERE ("step_type" = 'lens'::"text");



CREATE INDEX "idx_tools_id_key" ON "lenses"."tools" USING "btree" ("id", "key");



CREATE INDEX "idx_tutorial_completions_lenser" ON "lenses"."tutorial_completions" USING "btree" ("lenser_id", "kind");



CREATE INDEX "idx_version_parameters_version_id" ON "lenses"."version_parameters" USING "btree" ("version_id");



CREATE INDEX "idx_version_params_version_tool" ON "lenses"."version_parameters" USING "btree" ("version_id", "tool_id") WHERE ("tool_id" IS NOT NULL);



CREATE INDEX "idx_versions_lens_latest_covering" ON "lenses"."versions" USING "btree" ("lens_id", "version_number" DESC) INCLUDE ("id", "status", "published_at");



CREATE INDEX "idx_vpc_contents_gin" ON "lenses"."version_parameter_contents" USING "gin" ("contents");



CREATE INDEX "idx_vpc_lenser_workspace" ON "lenses"."version_parameter_contents" USING "btree" ("lenser_id", "workspace_id");



CREATE INDEX "idx_vpc_parameter_id" ON "lenses"."version_parameter_contents" USING "btree" ("parameter_id");



CREATE INDEX "idx_vpc_parameter_workspace_lenser" ON "lenses"."version_parameter_contents" USING "btree" ("parameter_id", "workspace_id", "lenser_id");



CREATE INDEX "idx_wf_edges_source" ON "lenses"."workflow_edges" USING "btree" ("source_node_id");



CREATE INDEX "idx_wf_edges_target" ON "lenses"."workflow_edges" USING "btree" ("target_node_id");



CREATE INDEX "idx_wf_edges_workflow" ON "lenses"."workflow_edges" USING "btree" ("workflow_id");



CREATE INDEX "idx_wf_nodes_workflow" ON "lenses"."workflow_nodes" USING "btree" ("workflow_id");



CREATE INDEX "idx_wf_parent_workflow" ON "lenses"."workflows" USING "btree" ("parent_workflow_id") WHERE ("parent_workflow_id" IS NOT NULL);



CREATE INDEX "idx_wf_runs_status_active" ON "lenses"."workflow_runs" USING "btree" ("status") WHERE ("status" = ANY (ARRAY['pending'::"text", 'running'::"text"]));



CREATE INDEX "idx_wf_runs_triggered_by" ON "lenses"."workflow_runs" USING "btree" ("triggered_by");



CREATE INDEX "idx_wf_runs_workflow" ON "lenses"."workflow_runs" USING "btree" ("workflow_id");



CREATE INDEX "idx_wnr_run" ON "lenses"."workflow_node_results" USING "btree" ("run_id");



CREATE INDEX "idx_wnr_run_status" ON "lenses"."workflow_node_results" USING "btree" ("run_id", "status");



CREATE INDEX "idx_workflow_dlq_unresolved" ON "lenses"."workflow_run_dead_letters" USING "btree" ("created_at" DESC) WHERE ("resolved_at" IS NULL);



CREATE INDEX "idx_workflow_node_results_version_node" ON "lenses"."workflow_node_results" USING "btree" ("workflow_version_node_id");



CREATE INDEX "idx_workflow_phases_workflow_id" ON "lenses"."workflow_phases" USING "btree" ("workflow_id", "ordinal");



CREATE INDEX "idx_workflow_run_events_created" ON "lenses"."workflow_run_events" USING "btree" ("created_at" DESC);



CREATE INDEX "idx_workflow_run_events_run_event" ON "lenses"."workflow_run_events" USING "btree" ("run_id", "event_id");



CREATE INDEX "idx_workflow_run_provenance_source" ON "lenses"."workflow_run_provenance" USING "btree" ("source_run_id", "source_node_id");



CREATE INDEX "idx_workflow_run_provenance_target" ON "lenses"."workflow_run_provenance" USING "btree" ("target_run_id", "target_node_id");



CREATE INDEX "idx_workflow_run_tags_run_id" ON "lenses"."workflow_run_tags" USING "btree" ("run_id");



CREATE INDEX "idx_workflow_run_tags_tag_severity" ON "lenses"."workflow_run_tags" USING "btree" ("tag", "severity", "created_at" DESC);



CREATE INDEX "idx_workflow_runs_active_node" ON "lenses"."workflow_runs" USING "btree" ("active_node_id") WHERE ("active_node_id" IS NOT NULL);



CREATE INDEX "idx_workflow_runs_ai_lenser" ON "lenses"."workflow_runs" USING "btree" ("ai_lenser_id", "created_at" DESC) WHERE ("ai_lenser_id" IS NOT NULL);



CREATE INDEX "idx_workflow_runs_backfill_id" ON "lenses"."workflow_runs" USING "btree" ((("metadata" ->> 'backfill_id'::"text"))) WHERE ("metadata" ? 'backfill_id'::"text");



CREATE INDEX "idx_workflow_runs_heartbeat_stale" ON "lenses"."workflow_runs" USING "btree" ("heartbeat_at") WHERE ("status" = ANY (ARRAY['queued'::"text", 'running'::"text", 'streaming'::"text", 'recovered'::"text"]));



CREATE INDEX "idx_workflow_runs_idempotency_lookup" ON "lenses"."workflow_runs" USING "btree" ("workflow_id", "idempotency_key", "idempotency_expires_at") WHERE ("idempotency_key" IS NOT NULL);



CREATE INDEX "idx_workflow_runs_parent" ON "lenses"."workflow_runs" USING "btree" ("parent_run_id") WHERE ("parent_run_id" IS NOT NULL);



CREATE INDEX "idx_workflow_runs_schedule_active" ON "lenses"."workflow_runs" USING "btree" ("schedule_id", "status") WHERE ("schedule_id" IS NOT NULL);



CREATE UNIQUE INDEX "idx_workflow_runs_schedule_slot" ON "lenses"."workflow_runs" USING "btree" ("schedule_id", "scheduled_for") WHERE (("schedule_id" IS NOT NULL) AND ("scheduled_for" IS NOT NULL));



CREATE INDEX "idx_workflow_runs_version_snapshot" ON "lenses"."workflow_runs" USING "btree" ("workflow_id", "workflow_version_id", "workflow_snapshot_hash");



CREATE INDEX "idx_workflow_schedules_active" ON "lenses"."workflow_schedules" USING "btree" ("last_run_at" NULLS FIRST, "created_at") WHERE ("is_active" = true);



COMMENT ON INDEX "lenses"."idx_workflow_schedules_active" IS 'D7: covers fn_dispatch_scheduled_workflows loop predicate (is_active=true AND last_run_at < current minute), ordered by created_at for stable lock order across concurrent dispatchers.';



CREATE INDEX "idx_workflow_tasks_phase_id" ON "lenses"."workflow_tasks" USING "btree" ("phase_id", "ordinal");



CREATE INDEX "idx_workflow_tasks_workflow_id" ON "lenses"."workflow_tasks" USING "btree" ("workflow_id");



CREATE INDEX "idx_workflow_versions_status" ON "lenses"."workflow_versions" USING "btree" ("status");



CREATE INDEX "idx_workflow_versions_workflow" ON "lenses"."workflow_versions" USING "btree" ("workflow_id");



CREATE INDEX "idx_workflows_lenser" ON "lenses"."workflows" USING "btree" ("lenser_id");



CREATE INDEX "idx_workflows_lifecycle_owner" ON "lenses"."workflows" USING "btree" ("lenser_id", "visibility", "archived_at", "deleted_at");



CREATE INDEX "idx_workflows_visibility" ON "lenses"."workflows" USING "btree" ("visibility") WHERE ("visibility" = 'public'::"text");



CREATE INDEX "idx_wve_version" ON "lenses"."workflow_version_edges" USING "btree" ("workflow_version_id");



CREATE INDEX "idx_wvn_version" ON "lenses"."workflow_version_nodes" USING "btree" ("workflow_version_id");



CREATE INDEX "parameter_contracts_classification_idx" ON "lenses"."parameter_contracts" USING "btree" ("classification") WHERE ("classification" = ANY (ARRAY['protected'::"text", 'system'::"text"]));



CREATE INDEX "parameter_contracts_label_idx" ON "lenses"."parameter_contracts" USING "btree" ("label");



CREATE INDEX "parameter_contracts_tool_idx" ON "lenses"."parameter_contracts" USING "btree" ("tool_id");



CREATE INDEX "parameter_deprecations_to_hash_idx" ON "lenses"."parameter_deprecations" USING "btree" ("to_content_hash") WHERE ("to_content_hash" IS NOT NULL);



CREATE UNIQUE INDEX "uq_schedule_calendars_lenser_name" ON "lenses"."schedule_calendars" USING "btree" ("lenser_id", "name");



CREATE UNIQUE INDEX "versions_content_hash_uniq" ON "lenses"."versions" USING "btree" ("content_hash") WHERE ("content_hash" IS NOT NULL);



CREATE INDEX "idx_attachments_entity" ON "media"."attachments" USING "btree" ("entity_type", "entity_id");



CREATE INDEX "idx_attachments_object" ON "media"."attachments" USING "btree" ("object_id");



CREATE INDEX "idx_fk_attachments_attached_by" ON "media"."attachments" USING "btree" ("attached_by");



CREATE INDEX "idx_fk_objects_created_by" ON "media"."objects" USING "btree" ("created_by");



CREATE INDEX "idx_media_objects_request_id" ON "media"."objects" USING "btree" ("request_id") WHERE ("request_id" IS NOT NULL);



CREATE UNIQUE INDEX "idx_objects_bucket_key" ON "media"."objects" USING "btree" ("bucket", "object_key") WHERE (("bucket" IS NOT NULL) AND ("object_key" IS NOT NULL));



CREATE INDEX "idx_objects_lifecycle_active" ON "media"."objects" USING "btree" ("lifecycle_state") WHERE ("lifecycle_state" <> 'deleted'::"text");



CREATE INDEX "idx_objects_media_type" ON "media"."objects" USING "btree" ("media_type");



CREATE INDEX "idx_objects_owner" ON "media"."objects" USING "btree" ("owner_lenser_id");



CREATE INDEX "idx_objects_workspace" ON "media"."objects" USING "btree" ("workspace_id");



CREATE INDEX "idx_activity_org_created" ON "organizations"."activity_log" USING "btree" ("org_id", "created_at" DESC);



CREATE INDEX "idx_addresses_company" ON "organizations"."addresses" USING "btree" ("company_id");



CREATE INDEX "idx_addresses_country" ON "organizations"."addresses" USING "btree" ("country_iso2_code");



CREATE INDEX "idx_audit_org_occurred" ON "organizations"."audit_logs" USING "btree" ("org_id", "occurred_at" DESC);



CREATE INDEX "idx_fk_activity_log_lenser_id" ON "organizations"."activity_log" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_addresses_city_id" ON "organizations"."addresses" USING "btree" ("city_id");



CREATE INDEX "idx_fk_addresses_state_id" ON "organizations"."addresses" USING "btree" ("state_id");



CREATE INDEX "idx_fk_companies_tax_iso2_code" ON "organizations"."companies" USING "btree" ("tax_iso2_code");



CREATE INDEX "idx_fk_members_invited_by" ON "organizations"."members" USING "btree" ("invited_by");



CREATE INDEX "idx_fk_org_audit_logs_lenser_id" ON "organizations"."audit_logs" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_organizations_kill_switched_by" ON "organizations"."organizations" USING "btree" ("kill_switched_by");



CREATE INDEX "idx_fk_organizations_workspace_id" ON "organizations"."organizations" USING "btree" ("workspace_id");



CREATE INDEX "idx_members_lenser" ON "organizations"."members" USING "btree" ("lenser_id");



CREATE INDEX "idx_members_org_role" ON "organizations"."members" USING "btree" ("org_id", "role");



CREATE INDEX "idx_org_created_by" ON "organizations"."organizations" USING "btree" ("created_by");



CREATE INDEX "idx_org_status_active" ON "organizations"."organizations" USING "btree" ("status") WHERE ("status" = 'active'::"organizations"."status_enum");



CREATE INDEX "idx_prefs_country" ON "organizations"."preferences" USING "btree" ("iso2_code");



CREATE UNIQUE INDEX "uq_addresses_one_default_per_company" ON "organizations"."addresses" USING "btree" ("company_id") WHERE ("is_default" = true);



CREATE UNIQUE INDEX "uq_members_one_owner_per_org" ON "organizations"."members" USING "btree" ("org_id") WHERE ("role" = 'owner'::"organizations"."member_role_enum");



CREATE INDEX "idx_tool_invocation_logs_lenser_time" ON "platform"."tool_invocation_logs" USING "btree" ("ai_lenser_id", "invoked_at" DESC);



CREATE INDEX "idx_tool_invocation_logs_pending" ON "platform"."tool_invocation_logs" USING "btree" ("ai_lenser_id", "invoked_at" DESC) WHERE ("approval_status" = 'awaiting_approval'::"text");



CREATE INDEX "idx_tool_invocation_logs_run" ON "platform"."tool_invocation_logs" USING "btree" ("run_id") WHERE ("run_id" IS NOT NULL);



CREATE INDEX "idx_notif_agg_open" ON "public"."notification_aggregates" USING "btree" ("recipient_id", "notification_type", "entity_id", "window_end");



CREATE INDEX "idx_notif_prefs_lenser" ON "public"."notification_preferences" USING "btree" ("lenser_id");



CREATE INDEX "idx_notifications_lenser_all" ON "public"."notifications" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_notifications_lenser_unread" ON "public"."notifications" USING "btree" ("lenser_id", "created_at" DESC) WHERE ("read_at" IS NULL);



CREATE INDEX "idx_uoc_lenser_active" ON "public"."user_oauth_connections" USING "btree" ("lenser_id") WHERE (("is_active" = true) AND ("revoked_at" IS NULL));



CREATE INDEX "idx_uoc_lenser_provider" ON "public"."user_oauth_connections" USING "btree" ("lenser_id", "provider") WHERE ("is_active" = true);



CREATE INDEX "idx_uoc_ref_active" ON "public"."user_oauth_connections" USING "btree" ("ref") WHERE (("is_active" = true) AND ("revoked_at" IS NULL));



CREATE INDEX "idx_uoc_workspace_lenser_ref_active" ON "public"."user_oauth_connections" USING "btree" ("workspace_id", "lenser_id", "ref") WHERE (("is_active" = true) AND ("revoked_at" IS NULL));



CREATE INDEX "idx_wic_lenser_type" ON "public"."workflow_integration_credentials" USING "btree" ("lenser_id", "integration_type");



CREATE INDEX "idx_workflow_kv_store_expiry" ON "public"."workflow_kv_store" USING "btree" ("expires_at");



CREATE INDEX "idx_workflow_kv_store_lookup" ON "public"."workflow_kv_store" USING "btree" ("workflow_id", "key");



CREATE UNIQUE INDEX "idx_workflow_webhook_triggers_secret" ON "public"."workflow_webhook_triggers" USING "btree" ("secret");



CREATE INDEX "idx_workflow_webhook_triggers_workflow" ON "public"."workflow_webhook_triggers" USING "btree" ("workflow_id") WHERE ("is_active" = true);



CREATE INDEX "idx_contender_ratings_category_elo" ON "reputation"."contender_ratings" USING "btree" ("category", "elo_rating" DESC);



CREATE INDEX "idx_reputation_scores_type_rank" ON "reputation"."lenser_scores" USING "btree" ("score_type", "score" DESC);



CREATE INDEX "idx_vote_risk_flagged" ON "reputation"."vote_risk_scores" USING "btree" ("review_status", "risk_score" DESC) WHERE ("review_status" = ANY (ARRAY['flagged'::"text", 'excluded'::"text"]));



CREATE UNIQUE INDEX "uniq_lenser_scores_lenser_type" ON "reputation"."lenser_scores" USING "btree" ("lenser_id", "score_type");



CREATE INDEX "idx_status_components_service_id" ON "status"."components" USING "btree" ("service_id");



CREATE INDEX "idx_status_components_status" ON "status"."components" USING "btree" ("current_status", "sort_order");



CREATE INDEX "idx_status_incident_updates_incident_created_at" ON "status"."incident_updates" USING "btree" ("incident_id", "created_at" DESC);



CREATE INDEX "idx_status_incidents_service_id" ON "status"."incidents" USING "btree" ("service_id", "started_at" DESC);



CREATE INDEX "idx_status_incidents_status_started_at" ON "status"."incidents" USING "btree" ("status", "started_at" DESC);



CREATE INDEX "idx_status_maintenance_service_id" ON "status"."maintenance_windows" USING "btree" ("service_id", "starts_at");



CREATE INDEX "idx_status_maintenance_status" ON "status"."maintenance_windows" USING "btree" ("status", "starts_at");



CREATE INDEX "idx_status_services_public" ON "status"."services" USING "btree" ("is_public", "sort_order") WHERE ("is_public" = true);



CREATE INDEX "idx_status_services_status_sort_order" ON "status"."services" USING "btree" ("current_status", "sort_order");



CREATE INDEX "idx_status_snapshots_scope_date" ON "status"."daily_snapshots" USING "btree" ("scope_type", "scope_key", "snapshot_date" DESC);



CREATE INDEX "idx_status_uptime_rollups_scope_window" ON "status"."uptime_rollups" USING "btree" ("scope_type", "scope_key", "rollup_window");



CREATE INDEX "idx_fk_workspace_members_invited_by" ON "tenancy"."workspace_members" USING "btree" ("invited_by");



CREATE INDEX "idx_workspace_members_lenser" ON "tenancy"."workspace_members" USING "btree" ("lenser_id");



CREATE INDEX "idx_workspaces_org" ON "tenancy"."workspaces" USING "btree" ("org_id") WHERE ("org_id" IS NOT NULL);



CREATE INDEX "idx_workspaces_owner" ON "tenancy"."workspaces" USING "btree" ("owner_lenser_id");



CREATE INDEX "idx_pending_charges_account" ON "wallet"."pending_charges" USING "btree" ("account_id", "status");



CREATE INDEX "idx_pending_charges_lenser" ON "wallet"."pending_charges" USING "btree" ("lenser_id", "reserved_at" DESC);



CREATE INDEX "idx_pending_charges_status" ON "wallet"."pending_charges" USING "btree" ("status") WHERE ("status" = 'reserved'::"wallet"."charge_status_enum");



CREATE INDEX "idx_transactions_account_time" ON "wallet"."transactions" USING "btree" ("account_id", "created_at" DESC);



CREATE INDEX "idx_wallet_spending_account" ON "wallet"."spending_limits" USING "btree" ("account_id", "period");



CREATE UNIQUE INDEX "idx_wallet_tx_idempotency" ON "wallet"."transactions" USING "btree" ("idempotency_key") WHERE ("idempotency_key" IS NOT NULL);



CREATE INDEX "idx_wallet_tx_reference" ON "wallet"."transactions" USING "btree" ("reference_type", "reference_id") WHERE ("reference_id" IS NOT NULL);



CREATE UNIQUE INDEX "idx_wallet_tx_reference_idempotency" ON "wallet"."transactions" USING "btree" ("reference_type", "reference_id") WHERE ("reference_id" IS NOT NULL);



CREATE INDEX "idx_wallet_tx_type_created" ON "wallet"."transactions" USING "btree" ("tx_type", "created_at" DESC);



CREATE INDEX "idx_contributions_context" ON "xp"."contributions" USING "btree" ("context");



CREATE INDEX "idx_contributions_lenser" ON "xp"."contributions" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_contributions_xp_event_id" ON "xp"."contributions" USING "btree" ("xp_event_id");



CREATE INDEX "idx_fk_event_verifications_event_id" ON "xp"."event_verifications" USING "btree" ("event_id");



CREATE INDEX "idx_fk_level_ups_lenser_id" ON "xp"."level_ups" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_season_totals_lenser_id" ON "xp"."season_totals" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_xp_events_lenser_id" ON "xp"."events" USING "btree" ("lenser_id");



CREATE INDEX "idx_fk_xp_events_rule_id" ON "xp"."events" USING "btree" ("rule_id");



CREATE INDEX "idx_season_totals_leaderboard" ON "xp"."season_totals" USING "btree" ("season_id", "app_id", "total_xp" DESC);



CREATE INDEX "idx_xp_events_daily_cap" ON "xp"."events" USING "btree" ("lenser_id", "action_key", "app_id", "created_at" DESC);



CREATE INDEX "idx_xp_events_season_cap" ON "xp"."events" USING "btree" ("lenser_id", "action_key", "app_id", "season_id") WHERE (("season_id" IS NOT NULL) AND ("xp" > 0));



CREATE INDEX "idx_xp_events_season_id" ON "xp"."events" USING "btree" ("season_id") WHERE ("season_id" IS NOT NULL);



CREATE INDEX "idx_xp_events_source_ref" ON "xp"."events" USING "btree" ("source_ref_type", "source_ref_id") WHERE ("xp" > 0);



CREATE INDEX "idx_xp_totals_app_id_total_xp_desc" ON "xp"."totals" USING "btree" ("app_id", "total_xp" DESC);



CREATE INDEX "idx_xp_totals_lenser_id" ON "xp"."totals" USING "btree" ("lenser_id");



CREATE INDEX "xp_levels_app_level_idx" ON "xp"."levels" USING "btree" ("app_id", "level");



CREATE OR REPLACE VIEW "lenses"."vw_lens_version_history" AS
 SELECT "v"."id",
    "v"."lens_id",
    "v"."version_number",
    "v"."template_body",
    "v"."status",
    "v"."changelog",
    "v"."parent_version_id",
    "v"."published_at",
    "v"."created_at",
    ("count"("vp"."id"))::integer AS "parameter_count"
   FROM ("lenses"."versions" "v"
     LEFT JOIN "lenses"."version_parameters" "vp" ON (("vp"."version_id" = "v"."id")))
  GROUP BY "v"."id";



CREATE OR REPLACE TRIGGER "no_delete_action_logs" BEFORE DELETE ON "agents"."action_logs" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_action_logs" BEFORE UPDATE ON "agents"."action_logs" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_block_unsafe_agent_delete" BEFORE DELETE ON "agents"."ai_lensers" FOR EACH ROW EXECUTE FUNCTION "public"."trg_block_unsafe_artifact_delete"();



CREATE OR REPLACE TRIGGER "trg_emit_approval_granted" AFTER UPDATE OF "approval_status" ON "agents"."team_runs" FOR EACH ROW EXECUTE FUNCTION "automation"."trg_emit_approval_granted"();



CREATE OR REPLACE TRIGGER "trg_emit_approval_timed_out" AFTER UPDATE OF "approval_status" ON "agents"."team_runs" FOR EACH ROW EXECUTE FUNCTION "automation"."trg_emit_approval_timed_out"();



CREATE OR REPLACE TRIGGER "trg_model_binding_provider_check" BEFORE INSERT OR UPDATE OF "model_id" ON "agents"."model_bindings" FOR EACH ROW EXECUTE FUNCTION "agents"."check_model_binding_single_provider"();



CREATE OR REPLACE TRIGGER "trg_notify_agent_created" AFTER INSERT ON "agents"."ownerships" FOR EACH ROW WHEN (("new"."role" = 'owner'::"text")) EXECUTE FUNCTION "agents"."fn_trg_notify_agent_created"();



CREATE OR REPLACE TRIGGER "trg_notify_model_binding_changed" AFTER INSERT OR DELETE OR UPDATE OF "is_default" ON "agents"."model_bindings" FOR EACH ROW EXECUTE FUNCTION "agents"."trg_notify_model_binding_changed"();



CREATE OR REPLACE TRIGGER "trg_notify_policy_updated" AFTER UPDATE ON "agents"."policies" FOR EACH ROW EXECUTE FUNCTION "agents"."trg_notify_policy_updated"();



CREATE OR REPLACE TRIGGER "trg_notify_team_run_change" AFTER INSERT OR UPDATE OF "status" ON "agents"."team_runs" FOR EACH ROW EXECUTE FUNCTION "agents"."trg_notify_team_run_change"();



CREATE OR REPLACE TRIGGER "trg_team_messages_cap" BEFORE INSERT ON "agents"."team_messages" FOR EACH ROW EXECUTE FUNCTION "agents"."fn_enforce_team_messages_cap"();



CREATE OR REPLACE TRIGGER "trg_team_runs_apply_standing_approval" BEFORE INSERT ON "agents"."team_runs" FOR EACH ROW WHEN (("new"."approval_status" = 'pending'::"text")) EXECUTE FUNCTION "agents"."fn_apply_standing_approval"();



CREATE OR REPLACE TRIGGER "trg_team_runs_approval_pending_webhook" AFTER INSERT ON "agents"."team_runs" FOR EACH ROW WHEN (("new"."approval_status" = 'pending'::"text")) EXECUTE FUNCTION "agents"."fn_notify_approval_pending"();



CREATE OR REPLACE TRIGGER "trg_team_runs_capture_agent_snapshot" BEFORE INSERT OR UPDATE OF "ai_lenser_id" ON "agents"."team_runs" FOR EACH ROW EXECUTE FUNCTION "agents"."trg_team_runs_capture_agent_snapshot"();



CREATE OR REPLACE TRIGGER "trg_tools_registry_egress_guard" BEFORE INSERT OR UPDATE ON "agents"."tools_registry" FOR EACH ROW EXECUTE FUNCTION "agents"."fn_tools_registry_egress_guard"();



CREATE OR REPLACE TRIGGER "workspace_settings_paused_sync" BEFORE INSERT OR UPDATE ON "agents"."workspace_settings" FOR EACH ROW EXECUTE FUNCTION "agents"."fn_workspace_settings_paused_sync"();



CREATE OR REPLACE TRIGGER "trg_ai_providers_updated_at" BEFORE UPDATE ON "ai"."providers" FOR EACH ROW EXECUTE FUNCTION "battles"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_model_active_provider_guard" BEFORE INSERT OR UPDATE OF "is_active" ON "ai"."models" FOR EACH ROW EXECUTE FUNCTION "ai"."enforce_active_provider_for_model"();



COMMENT ON TRIGGER "trg_model_active_provider_guard" ON "ai"."models" IS 'Fires before INSERT or UPDATE of is_active on ai.models. Rejects activation of a model whose provider is currently inactive.';



CREATE OR REPLACE TRIGGER "trg_provider_deactivation_cascade" AFTER UPDATE OF "is_active" ON "ai"."providers" FOR EACH ROW EXECUTE FUNCTION "ai"."cascade_provider_deactivation"();



COMMENT ON TRIGGER "trg_provider_deactivation_cascade" ON "ai"."providers" IS 'Fires after UPDATE of is_active on ai.providers. Cascades deactivation to all models of the deactivated provider.';



CREATE OR REPLACE TRIGGER "trg_lenser_join_award_badges" AFTER INSERT ON "analytics"."lenser_join_log" FOR EACH ROW EXECUTE FUNCTION "lensers"."trg_award_founder_badges"();



CREATE OR REPLACE TRIGGER "trg_no_delete_lenser_join_log" BEFORE DELETE ON "analytics"."lenser_join_log" FOR EACH ROW EXECUTE FUNCTION "lensers"."prevent_lenser_join_log_delete"();



CREATE OR REPLACE TRIGGER "trg_no_update_lenser_join_log" BEFORE UPDATE ON "analytics"."lenser_join_log" FOR EACH ROW EXECUTE FUNCTION "lensers"."prevent_lenser_join_log_update"();



CREATE OR REPLACE TRIGGER "trg_protect_feedback_system_fields" BEFORE UPDATE ON "analytics"."product_feedback" FOR EACH ROW EXECUTE FUNCTION "analytics"."protect_feedback_system_fields"();



CREATE OR REPLACE TRIGGER "trg_set_feedback_user_id" BEFORE INSERT ON "analytics"."product_feedback" FOR EACH ROW EXECUTE FUNCTION "analytics"."set_feedback_user_id"();



CREATE OR REPLACE TRIGGER "trg_sync_join_order" AFTER INSERT ON "analytics"."lenser_join_log" FOR EACH ROW EXECUTE FUNCTION "lensers"."sync_join_order"();



CREATE OR REPLACE TRIGGER "no_delete_attestations" BEFORE DELETE ON "audit"."attestations" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_delete_audit_hash_chains" BEFORE DELETE ON "audit"."hash_chains" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_delete_hash_chains" BEFORE DELETE ON "audit"."hash_chains" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_attestations" BEFORE UPDATE ON "audit"."attestations" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_audit_hash_chains" BEFORE UPDATE ON "audit"."hash_chains" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_hash_chains" BEFORE UPDATE ON "audit"."hash_chains" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_audit_events_backfill_actor" BEFORE INSERT ON "audit"."events" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_backfill_unified_actor"();



CREATE OR REPLACE TRIGGER "trg_emit_moderation_flagged" AFTER INSERT ON "audit"."moderation_decisions" FOR EACH ROW WHEN (("new"."decision_type" = 'flagged'::"text")) EXECUTE FUNCTION "automation"."trg_emit_moderation_flagged"();



CREATE OR REPLACE TRIGGER "trg_moderation_decisions_flagged_webhook" AFTER INSERT ON "audit"."moderation_decisions" FOR EACH ROW WHEN (("new"."decision_type" = 'flagged'::"text")) EXECUTE FUNCTION "audit"."fn_notify_moderation_flagged"();



CREATE OR REPLACE TRIGGER "trg_prevent_delete_account_lifecycle" BEFORE DELETE ON "audit"."account_lifecycle" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_prevent_delete"();



CREATE OR REPLACE TRIGGER "trg_prevent_delete_admin" BEFORE DELETE ON "audit"."admin_actions" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_prevent_delete"();



CREATE OR REPLACE TRIGGER "trg_prevent_delete_events" BEFORE DELETE ON "audit"."events" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_prevent_delete"();



CREATE OR REPLACE TRIGGER "trg_prevent_delete_moderation" BEFORE DELETE ON "audit"."moderation_decisions" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_prevent_delete"();



CREATE OR REPLACE TRIGGER "trg_prevent_delete_security" BEFORE DELETE ON "audit"."security_events" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_prevent_delete"();



CREATE OR REPLACE TRIGGER "trg_trigger_rules_touch_updated_at" BEFORE UPDATE ON "automation"."trigger_rules" FOR EACH ROW EXECUTE FUNCTION "automation"."trg_trigger_rules_touch_updated_at"();



CREATE OR REPLACE TRIGGER "no_delete_rule_snapshots" BEFORE DELETE ON "battles"."rule_snapshots" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_rule_snapshots" BEFORE UPDATE ON "battles"."rule_snapshots" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_ai_lenser_battle_xp" AFTER UPDATE ON "battles"."battles" FOR EACH ROW WHEN ((("new"."status" = 'closed'::"battles"."battle_status_enum") AND ("old"."status" <> 'closed'::"battles"."battle_status_enum"))) EXECUTE FUNCTION "xp"."trg_ai_lenser_battle_xp_fn"();



CREATE OR REPLACE TRIGGER "trg_award_battle_xp" AFTER UPDATE ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "battles"."award_battle_xp"();



CREATE OR REPLACE TRIGGER "trg_battle_execution_jobs_capture_agent_snapshot" BEFORE INSERT OR UPDATE OF "contender_id" ON "battles"."battle_execution_jobs" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_battle_execution_jobs_capture_agent_snapshot"();



CREATE OR REPLACE TRIGGER "trg_battle_status_audit" AFTER UPDATE ON "battles"."battles" FOR EACH ROW WHEN (("old"."status" IS DISTINCT FROM "new"."status")) EXECUTE FUNCTION "audit"."trg_battle_status_audit"();



CREATE OR REPLACE TRIGGER "trg_battle_status_event" AFTER UPDATE ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "battles"."log_battle_status_event"();



CREATE OR REPLACE TRIGGER "trg_battle_structural_immutability" BEFORE UPDATE ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_battle_structural_immutability"();



CREATE OR REPLACE TRIGGER "trg_battles_require_challenge_on_start" BEFORE UPDATE ON "battles"."battles" FOR EACH ROW WHEN (("old"."status" IS DISTINCT FROM "new"."status")) EXECUTE FUNCTION "battles"."trg_battle_start_requires_challenge"();



CREATE OR REPLACE TRIGGER "trg_battles_status_webhook" AFTER UPDATE OF "status" ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "public"."fn_trg_battles_status_webhook"();



CREATE OR REPLACE TRIGGER "trg_battles_updated_at" BEFORE UPDATE ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "battles"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_block_unsafe_battle_delete" BEFORE DELETE ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "public"."trg_block_unsafe_artifact_delete"();



CREATE OR REPLACE TRIGGER "trg_comments_updated_at" BEFORE UPDATE ON "battles"."comments" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_touch_comment_updated_at"();



CREATE OR REPLACE TRIGGER "trg_contender_entity_map_capture_agent_snapshot" BEFORE INSERT OR UPDATE OF "ai_lenser_id" ON "battles"."contender_entity_map" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_contender_entity_map_capture_agent_snapshot"();



CREATE OR REPLACE TRIGGER "trg_contender_runs_updated_at" BEFORE UPDATE ON "battles"."contender_runs" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_cost_ledgers_no_delete" BEFORE DELETE ON "battles"."cost_ledgers" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_events_immutable"();



CREATE OR REPLACE TRIGGER "trg_cost_ledgers_no_update" BEFORE UPDATE ON "battles"."cost_ledgers" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_events_immutable"();



CREATE OR REPLACE TRIGGER "trg_elo_on_battle_finalize" AFTER UPDATE ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "reputation"."fn_update_elo_on_finalize"();



CREATE OR REPLACE TRIGGER "trg_emit_battle_finalized" AFTER UPDATE OF "status" ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "automation"."trg_emit_battle_finalized"();



CREATE OR REPLACE TRIGGER "trg_enforce_status_transition" BEFORE UPDATE OF "status" ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_enforce_status_transition"();



CREATE OR REPLACE TRIGGER "trg_events_no_delete" BEFORE DELETE ON "battles"."events" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_events_immutable"();



CREATE OR REPLACE TRIGGER "trg_events_no_update" BEFORE UPDATE ON "battles"."events" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_events_immutable"();



CREATE OR REPLACE TRIGGER "trg_funding_policies_updated_at" BEFORE UPDATE ON "battles"."funding_policies" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_generated_challenges_immutable" BEFORE UPDATE ON "battles"."generated_challenges" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_generated_challenge_immutable"();



CREATE OR REPLACE TRIGGER "trg_handicap_updated_at" BEFORE UPDATE ON "battles"."ai_handicap_policies" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_update_handicap_timestamp"();



CREATE OR REPLACE TRIGGER "trg_model_test_runs_no_update" BEFORE DELETE OR UPDATE ON "battles"."model_test_runs" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_model_test_runs_immutable"();



CREATE OR REPLACE TRIGGER "trg_notify_agent_battle_won" AFTER UPDATE OF "winner_contender_id" ON "battles"."battles" FOR EACH ROW WHEN ((("old"."winner_contender_id" IS DISTINCT FROM "new"."winner_contender_id") AND ("new"."winner_contender_id" IS NOT NULL))) EXECUTE FUNCTION "battles"."fn_trg_notify_agent_battle_won"();



CREATE OR REPLACE TRIGGER "trg_notify_ai_vote_cast" AFTER INSERT ON "battles"."votes" FOR EACH ROW WHEN (("new"."is_ai_vote" = true)) EXECUTE FUNCTION "battles"."trg_notify_ai_vote_cast"();



CREATE OR REPLACE TRIGGER "trg_notify_battle_assigned" AFTER INSERT ON "battles"."contender_entity_map" FOR EACH ROW WHEN (("new"."ai_lenser_id" IS NOT NULL)) EXECUTE FUNCTION "battles"."trg_notify_battle_assigned"();



CREATE OR REPLACE TRIGGER "trg_notify_battle_comment" AFTER INSERT ON "battles"."comments" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_trg_notify_battle_comment"();



CREATE OR REPLACE TRIGGER "trg_notify_battle_joined" AFTER INSERT ON "battles"."contender_entity_map" FOR EACH ROW WHEN (("new"."profile_id" IS NOT NULL)) EXECUTE FUNCTION "battles"."fn_trg_notify_battle_joined"();



CREATE OR REPLACE TRIGGER "trg_notify_battle_started" AFTER UPDATE OF "status" ON "battles"."battles" FOR EACH ROW WHEN ((("old"."status" IS DISTINCT FROM "new"."status") AND ("new"."status" = 'open'::"battles"."battle_status_enum"))) EXECUTE FUNCTION "battles"."fn_trg_notify_battle_started"();



CREATE OR REPLACE TRIGGER "trg_notify_template_battle_status" AFTER UPDATE OF "status" ON "battles"."battles" FOR EACH ROW WHEN (("new"."template_id" IS NOT NULL)) EXECUTE FUNCTION "battles"."fn_trg_template_battle_status_notify"();



COMMENT ON TRIGGER "trg_notify_template_battle_status" ON "battles"."battles" IS 'Phase BF: notifies the creator when a template-sourced battle opens / is published. Skips ad-hoc battles where template_id IS NULL.';



CREATE OR REPLACE TRIGGER "trg_notify_vote_received" AFTER INSERT ON "battles"."votes" FOR EACH ROW WHEN (("new"."is_ai_vote" = false)) EXECUTE FUNCTION "battles"."trg_notify_vote_received"();



CREATE OR REPLACE TRIGGER "trg_populate_contender_entity_map" AFTER INSERT ON "battles"."contenders" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_populate_contender_entity_map"();



CREATE OR REPLACE TRIGGER "trg_prevent_self_vote" BEFORE INSERT ON "battles"."votes" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_prevent_self_vote"();



CREATE OR REPLACE TRIGGER "trg_rubrics_updated_at" BEFORE UPDATE ON "battles"."rubrics" FOR EACH ROW EXECUTE FUNCTION "battles"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_seed_vote_risk_on_insert" AFTER INSERT ON "battles"."votes" FOR EACH ROW EXECUTE FUNCTION "reputation"."fn_seed_vote_risk"();



CREATE OR REPLACE TRIGGER "trg_snapshot_battle_on_first_open" AFTER UPDATE OF "status" ON "battles"."battles" FOR EACH ROW WHEN (("old"."status" IS DISTINCT FROM "new"."status")) EXECUTE FUNCTION "battles"."trg_snapshot_battle_on_first_open"();



CREATE OR REPLACE TRIGGER "trg_submissions_updated_at" BEFORE UPDATE ON "battles"."submissions" FOR EACH ROW EXECUTE FUNCTION "battles"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_submissions_validate_execution_link" BEFORE INSERT OR UPDATE ON "battles"."submissions" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_submissions_validate_execution_link"();



CREATE OR REPLACE TRIGGER "trg_templates_updated_at" BEFORE UPDATE ON "battles"."templates" FOR EACH ROW EXECUTE FUNCTION "battles"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_validate_comment_lenser" BEFORE INSERT ON "battles"."comments" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_validate_comment_lenser"();



CREATE OR REPLACE TRIGGER "trg_vote_refresh_velocity" AFTER INSERT ON "battles"."votes" FOR EACH ROW EXECUTE FUNCTION "battles"."trg_refresh_vote_velocity"();



CREATE OR REPLACE TRIGGER "trg_votes_set_updated_at" BEFORE UPDATE ON "battles"."votes" FOR EACH ROW EXECUTE FUNCTION "battles"."fn_votes_set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_votes_velocity_check" BEFORE INSERT ON "battles"."votes" FOR EACH ROW EXECUTE FUNCTION "public"."fn_trg_votes_velocity_check"();



CREATE OR REPLACE TRIGGER "trg_xp_battle_created" AFTER INSERT ON "battles"."battles" FOR EACH ROW EXECUTE FUNCTION "xp"."on_battle_created"();

ALTER TABLE "battles"."battles" DISABLE TRIGGER "trg_xp_battle_created";



CREATE OR REPLACE TRIGGER "trg_xp_battle_published" AFTER UPDATE OF "status" ON "battles"."battles" FOR EACH ROW WHEN ((("old"."status" = 'draft'::"battles"."battle_status_enum") AND ("new"."status" <> 'draft'::"battles"."battle_status_enum"))) EXECUTE FUNCTION "xp"."on_battle_created"();

ALTER TABLE "battles"."battles" DISABLE TRIGGER "trg_xp_battle_published";



CREATE OR REPLACE TRIGGER "trg_billing_margin_policies_updated_at" BEFORE UPDATE ON "billing"."execution_margin_policies" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_billing_products_updated_at" BEFORE UPDATE ON "billing"."products" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_ledger_entries_hash" BEFORE INSERT ON "billing"."ledger_entries" FOR EACH ROW EXECUTE FUNCTION "billing"."tg_ledger_entries_hash"();



CREATE OR REPLACE TRIGGER "trg_ledger_entries_no_delete" BEFORE DELETE ON "billing"."ledger_entries" FOR EACH ROW EXECUTE FUNCTION "billing"."tg_ledger_entries_immutable"();



CREATE OR REPLACE TRIGGER "trg_ledger_entries_no_update" BEFORE UPDATE ON "billing"."ledger_entries" FOR EACH ROW EXECUTE FUNCTION "billing"."tg_ledger_entries_immutable"();



CREATE OR REPLACE TRIGGER "thread_replies_after_delete" AFTER DELETE ON "content"."thread_replies" FOR EACH ROW EXECUTE FUNCTION "content"."thread_replies_after_delete_trigger"();



CREATE OR REPLACE TRIGGER "thread_replies_after_insert" AFTER INSERT ON "content"."thread_replies" FOR EACH ROW EXECUTE FUNCTION "content"."thread_replies_after_insert_trigger"();



CREATE OR REPLACE TRIGGER "thread_replies_after_update" AFTER UPDATE ON "content"."thread_replies" FOR EACH ROW EXECUTE FUNCTION "content"."thread_replies_after_update_trigger"();



CREATE OR REPLACE TRIGGER "trg_check_reaction_target" BEFORE INSERT OR UPDATE OF "entity_type", "entity_id" ON "content"."reactions" FOR EACH ROW EXECUTE FUNCTION "content"."fn_check_reaction_target"();



CREATE OR REPLACE TRIGGER "trg_cleanup_thread_content_refs" AFTER DELETE ON "content"."threads" FOR EACH ROW EXECUTE FUNCTION "content"."trg_cleanup_thread_content_refs"();



CREATE OR REPLACE TRIGGER "trg_cleanup_thread_reply_content_refs" AFTER DELETE ON "content"."thread_replies" FOR EACH ROW EXECUTE FUNCTION "content"."trg_cleanup_thread_reply_content_refs"();



CREATE OR REPLACE TRIGGER "trg_content_threads_updated_at" BEFORE UPDATE ON "content"."threads" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_notify_lens_comment" AFTER INSERT ON "content"."thread_replies" FOR EACH ROW WHEN ((("new"."deleted_at" IS NULL) AND ("new"."status" = 'published'::"content"."thread_reply_status"))) EXECUTE FUNCTION "content"."fn_trg_notify_lens_comment"();



CREATE OR REPLACE TRIGGER "trg_notify_lens_reaction" AFTER INSERT ON "content"."reactions" FOR EACH ROW WHEN (("new"."entity_type" = ANY (ARRAY['lens'::"content"."entity_type_enum", 'workflow'::"content"."entity_type_enum"]))) EXECUTE FUNCTION "content"."trg_notify_lens_reaction"();



CREATE OR REPLACE TRIGGER "trg_sync_thread_count" AFTER INSERT OR DELETE ON "content"."threads" FOR EACH ROW EXECUTE FUNCTION "content"."sync_thread_count"();



CREATE OR REPLACE TRIGGER "trg_tag_created" AFTER INSERT ON "content"."tag_map" FOR EACH ROW EXECUTE FUNCTION "xp"."on_tag_created"();



CREATE OR REPLACE TRIGGER "trg_workflow_reaction_totals" AFTER INSERT OR DELETE ON "content"."reactions" FOR EACH ROW EXECUTE FUNCTION "content"."update_workflow_reaction_counters"();



CREATE OR REPLACE TRIGGER "trg_xp_on_reply_created" AFTER INSERT ON "content"."thread_replies" FOR EACH ROW EXECUTE FUNCTION "xp"."on_reply_created"();



CREATE OR REPLACE TRIGGER "trg_xp_on_reply_received" AFTER INSERT ON "content"."thread_replies" FOR EACH ROW EXECUTE FUNCTION "xp"."on_reply_received"();



CREATE OR REPLACE TRIGGER "trg_xp_on_thread_created" AFTER INSERT ON "content"."threads" FOR EACH ROW EXECUTE FUNCTION "xp"."on_thread_created"();



CREATE OR REPLACE TRIGGER "trg_xp_on_workflow_reaction" AFTER INSERT ON "content"."reactions" FOR EACH ROW WHEN (("new"."entity_type" = 'workflow'::"content"."entity_type_enum")) EXECUTE FUNCTION "content"."fn_xp_on_workflow_reaction"();



CREATE OR REPLACE TRIGGER "trg_xp_thread_visibility_changed" AFTER UPDATE OF "visibility", "status" ON "content"."threads" FOR EACH ROW EXECUTE FUNCTION "xp"."on_thread_visibility_changed"();



CREATE OR REPLACE TRIGGER "chain_on_device_registered" AFTER INSERT ON "devices"."registered_devices" FOR EACH ROW EXECUTE FUNCTION "devices"."fn_chain_on_device_registered"();



CREATE OR REPLACE TRIGGER "chain_on_device_trust_changed" AFTER UPDATE OF "trust_level" ON "devices"."registered_devices" FOR EACH ROW EXECUTE FUNCTION "devices"."fn_chain_on_device_trust_changed"();



CREATE OR REPLACE TRIGGER "xp_on_device_registered" AFTER INSERT ON "devices"."registered_devices" FOR EACH ROW EXECUTE FUNCTION "devices"."fn_xp_on_device_registered"();



CREATE OR REPLACE TRIGGER "xp_on_device_trust_elevated" AFTER UPDATE OF "trust_level" ON "devices"."registered_devices" FOR EACH ROW EXECUTE FUNCTION "devices"."fn_xp_on_device_trust_elevated"();



CREATE OR REPLACE TRIGGER "chain_on_attestation_verified" AFTER INSERT ON "execution"."attestation_verifications" FOR EACH ROW EXECUTE FUNCTION "execution"."fn_chain_on_attestation_verified"();



CREATE OR REPLACE TRIGGER "execution_links_no_update" BEFORE UPDATE ON "execution"."links" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_delete_att_verifications" BEFORE DELETE ON "execution"."attestation_verifications" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_delete_exec_attestations" BEFORE DELETE ON "execution"."attestations" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_att_verifications" BEFORE UPDATE ON "execution"."attestation_verifications" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "no_update_exec_attestations" BEFORE UPDATE ON "execution"."attestations" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_audit_artifact_visibility" AFTER UPDATE OF "visibility" ON "execution"."artifacts" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_artifact_visibility_changed_fn"();



CREATE OR REPLACE TRIGGER "trg_audit_execution_run_created" AFTER INSERT ON "execution"."runs" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_execution_run_created_fn"();



CREATE OR REPLACE TRIGGER "trg_requests_resolve_lens_snapshot" BEFORE INSERT ON "execution"."requests" FOR EACH ROW EXECUTE FUNCTION "execution"."trg_requests_resolve_lens_snapshot"();



CREATE OR REPLACE TRIGGER "trg_runs_set_completed_at" BEFORE UPDATE ON "execution"."runs" FOR EACH ROW EXECUTE FUNCTION "execution"."trg_runs_set_completed_at"();



CREATE OR REPLACE TRIGGER "xp_on_full_trust" AFTER INSERT OR UPDATE OF "trust_level" ON "execution"."trust_evaluations" FOR EACH ROW EXECUTE FUNCTION "execution"."fn_xp_on_full_trust"();



CREATE OR REPLACE TRIGGER "xp_on_runner_bound" AFTER INSERT OR UPDATE OF "status" ON "execution"."runner_device_bindings" FOR EACH ROW EXECUTE FUNCTION "execution"."fn_xp_on_runner_bound"();



CREATE OR REPLACE TRIGGER "xp_on_submission_evaluated" AFTER INSERT ON "execution"."trust_evaluations" FOR EACH ROW EXECUTE FUNCTION "execution"."fn_xp_on_submission_evaluated"();



CREATE OR REPLACE TRIGGER "trg_after_lenser_insert" AFTER INSERT ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."trg_create_join_log"();



CREATE OR REPLACE TRIGGER "trg_anonymize_join_log" AFTER DELETE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."anonymize_join_log"();



CREATE OR REPLACE TRIGGER "trg_audit_group_membership" AFTER INSERT OR DELETE OR UPDATE ON "lensers"."group_members" FOR EACH ROW EXECUTE FUNCTION "lensers"."trg_audit_group_membership"();



CREATE OR REPLACE TRIGGER "trg_audit_lenser_profile" AFTER DELETE OR UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_fn_audit_lenser_profile"();



CREATE OR REPLACE TRIGGER "trg_auto_create_wallet_account" AFTER INSERT ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "wallet"."trg_auto_create_account"();



CREATE OR REPLACE TRIGGER "trg_create_default_preferences" AFTER INSERT ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."fn_create_default_preferences"();



COMMENT ON TRIGGER "trg_create_default_preferences" ON "lensers"."profiles" IS 'Auto-creates a lensers.preferences row with default values for every new profile.';



CREATE OR REPLACE TRIGGER "trg_enforce_lensers_protections" BEFORE INSERT OR UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."enforce_lensers_protections"();



CREATE OR REPLACE TRIGGER "trg_groups_updated_at" BEFORE UPDATE ON "lensers"."groups" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_guard_lenser_handle" BEFORE INSERT OR UPDATE OF "handle" ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "identity_gov"."fn_guard_lenser_handle"();



CREATE OR REPLACE TRIGGER "trg_guard_profile_identity" BEFORE UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."guard_profile_identity_columns"();



CREATE OR REPLACE TRIGGER "trg_handle_deletion_request" BEFORE UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."trg_handle_deletion_request"();



CREATE OR REPLACE TRIGGER "trg_init_lenser_engagement_row" AFTER INSERT ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."init_lenser_engagement_row"();



CREATE OR REPLACE TRIGGER "trg_log_lenser_join" AFTER INSERT ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."log_lenser_join"();



CREATE OR REPLACE TRIGGER "trg_normalize_website_url" BEFORE INSERT OR UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "content"."normalize_website_url"();



CREATE OR REPLACE TRIGGER "trg_notify_badge_awarded" AFTER INSERT ON "lensers"."badges" FOR EACH ROW EXECUTE FUNCTION "lensers"."fn_trg_notify_badge_awarded"();



CREATE OR REPLACE TRIGGER "trg_notify_relationship_change" AFTER INSERT OR UPDATE ON "lensers"."relationships" FOR EACH ROW WHEN ((("new"."status" = ANY (ARRAY['pending'::"lensers"."relationship_status", 'accepted'::"lensers"."relationship_status"])) AND ("new"."removed_at" IS NULL))) EXECUTE FUNCTION "lensers"."trg_notify_relationship_change"();



CREATE OR REPLACE TRIGGER "trg_preferences_updated_at" BEFORE UPDATE ON "lensers"."preferences" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



COMMENT ON TRIGGER "trg_preferences_updated_at" ON "lensers"."preferences" IS 'Automatically updates updated_at on every row change.';



CREATE OR REPLACE TRIGGER "trg_prevent_preferences_delete" BEFORE DELETE ON "lensers"."preferences" FOR EACH ROW EXECUTE FUNCTION "lensers"."fn_prevent_preferences_delete"();



COMMENT ON TRIGGER "trg_prevent_preferences_delete" ON "lensers"."preferences" IS 'Enforces the invariant: a lenser must always have a preferences row. Direct DELETE is blocked; only profile cascade delete is permitted.';



CREATE OR REPLACE TRIGGER "trg_prevent_self_relationship" BEFORE INSERT OR UPDATE ON "lensers"."relationships" FOR EACH ROW EXECUTE FUNCTION "lensers"."fn_prevent_self_relationship"();



COMMENT ON TRIGGER "trg_prevent_self_relationship" ON "lensers"."relationships" IS 'Fires before INSERT or UPDATE. Raises 23514 if source = target. Delegates to lensers.fn_prevent_self_relationship(). Added in migration 000056; re-registered cleanly in migration 000058.';



CREATE OR REPLACE TRIGGER "trg_profiles_create_personal_workspace" AFTER INSERT OR UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "tenancy"."fn_create_personal_workspace"();



CREATE OR REPLACE TRIGGER "trg_protect_sensitive_lenser_fields" BEFORE UPDATE ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."protect_sensitive_lenser_fields"();



CREATE OR REPLACE TRIGGER "trg_relationships_sync_counts" AFTER INSERT OR DELETE OR UPDATE ON "lensers"."relationships" FOR EACH ROW EXECUTE FUNCTION "lensers"."fn_sync_relationship_counts"();



CREATE OR REPLACE TRIGGER "trg_sync_profile_from_auth_metadata" BEFORE INSERT ON "lensers"."profiles" FOR EACH ROW EXECUTE FUNCTION "lensers"."sync_profile_from_auth_metadata"();



CREATE OR REPLACE TRIGGER "trg_xp_on_follow" AFTER INSERT OR UPDATE OF "status" ON "lensers"."relationships" FOR EACH ROW EXECUTE FUNCTION "lensers"."fn_xp_on_follow"();



CREATE OR REPLACE TRIGGER "tg_xp_lens_created" AFTER INSERT ON "lenses"."lenses" FOR EACH ROW EXECUTE FUNCTION "xp"."on_lens_created"();



CREATE OR REPLACE TRIGGER "trg_accumulate_node_cost" AFTER UPDATE ON "lenses"."workflow_node_results" FOR EACH ROW EXECUTE FUNCTION "lenses"."trg_accumulate_node_cost_fn"();



CREATE OR REPLACE TRIGGER "trg_audit_workflow_run" AFTER INSERT OR UPDATE ON "lenses"."workflow_runs" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_workflow_run_lifecycle_fn"();



CREATE OR REPLACE TRIGGER "trg_block_unsafe_lens_delete" BEFORE DELETE ON "lenses"."lenses" FOR EACH ROW EXECUTE FUNCTION "public"."trg_block_unsafe_artifact_delete"();



CREATE OR REPLACE TRIGGER "trg_block_unsafe_workflow_delete" BEFORE DELETE ON "lenses"."workflows" FOR EACH ROW EXECUTE FUNCTION "public"."trg_block_unsafe_artifact_delete"();



CREATE OR REPLACE TRIGGER "trg_cleanup_lens_content_refs" AFTER DELETE ON "lenses"."lenses" FOR EACH ROW EXECUTE FUNCTION "lenses"."trg_cleanup_lens_content_refs"();



CREATE OR REPLACE TRIGGER "trg_contract_signatures_immutable" BEFORE DELETE OR UPDATE ON "lenses"."contract_signatures" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_contracts_immutable" BEFORE DELETE OR UPDATE ON "lenses"."contracts" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_dependency_edges_immutable" BEFORE DELETE OR UPDATE ON "lenses"."dependency_edges" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_emit_workflow_run_state" AFTER UPDATE OF "status" ON "lenses"."workflow_runs" FOR EACH ROW EXECUTE FUNCTION "automation"."trg_emit_workflow_run_state"();



CREATE OR REPLACE TRIGGER "trg_execution_records_immutable" BEFORE DELETE OR UPDATE ON "lenses"."execution_records" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_lens_steps_updated_at" BEFORE UPDATE ON "lenses"."steps" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_lenses_tools_updated_at" BEFORE UPDATE ON "lenses"."tools" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_lenses_updated_at" BEFORE UPDATE ON "lenses"."lenses" FOR EACH ROW EXECUTE FUNCTION "content"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_notify_cron_run_change" AFTER UPDATE OF "status" ON "lenses"."workflow_runs" FOR EACH ROW EXECUTE FUNCTION "lenses"."trg_notify_cron_run_change"();



CREATE OR REPLACE TRIGGER "trg_notify_lens_featured" AFTER UPDATE OF "is_featured" ON "lenses"."lenses" FOR EACH ROW WHEN ((("old"."is_featured" = false) AND ("new"."is_featured" = true))) EXECUTE FUNCTION "lenses"."fn_trg_notify_lens_featured"();



CREATE OR REPLACE TRIGGER "trg_notify_lens_forked" AFTER INSERT ON "lenses"."lenses" FOR EACH ROW WHEN (("new"."parent_lens_id" IS NOT NULL)) EXECUTE FUNCTION "lenses"."fn_trg_notify_lens_forked"();



CREATE OR REPLACE TRIGGER "trg_notify_lens_published" AFTER UPDATE OF "status" ON "lenses"."lenses" FOR EACH ROW WHEN ((("old"."status" IS DISTINCT FROM "new"."status") AND ("new"."status" = 'published'::"content"."content_status"))) EXECUTE FUNCTION "lenses"."fn_trg_notify_lens_published"();



CREATE OR REPLACE TRIGGER "trg_notify_workflow_forked" AFTER INSERT ON "lenses"."workflows" FOR EACH ROW WHEN (("new"."parent_workflow_id" IS NOT NULL)) EXECUTE FUNCTION "lenses"."fn_trg_notify_workflow_forked"();



CREATE OR REPLACE TRIGGER "trg_notify_workflow_published" AFTER UPDATE OF "visibility" ON "lenses"."workflows" FOR EACH ROW WHEN ((("old"."visibility" IS DISTINCT FROM "new"."visibility") AND ("new"."visibility" = 'public'::"text"))) EXECUTE FUNCTION "lenses"."fn_trg_notify_workflow_published"();



CREATE OR REPLACE TRIGGER "trg_parameter_contracts_immutable" BEFORE DELETE OR UPDATE ON "lenses"."parameter_contracts" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_parameter_deprecations_immutable" BEFORE DELETE OR UPDATE ON "lenses"."parameter_deprecations" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_prevent_published_version_update" BEFORE UPDATE ON "lenses"."versions" FOR EACH ROW EXECUTE FUNCTION "lenses"."fn_prevent_published_version_update"();



CREATE OR REPLACE TRIGGER "trg_schedule_calendars_touch_updated" BEFORE UPDATE ON "lenses"."schedule_calendars" FOR EACH ROW EXECUTE FUNCTION "lenses"."fn_schedule_calendars_touch_updated"();



CREATE OR REPLACE TRIGGER "trg_security_scopes_immutable" BEFORE DELETE OR UPDATE ON "lenses"."security_scopes" FOR EACH ROW EXECUTE FUNCTION "public"."fn_deny_mutation"();



CREATE OR REPLACE TRIGGER "trg_steps_lens_id_sync" BEFORE INSERT OR UPDATE ON "lenses"."steps" FOR EACH ROW EXECUTE FUNCTION "lenses"."trg_steps_sync_lens_id"();



CREATE OR REPLACE TRIGGER "trg_sync_lens_count" AFTER INSERT OR DELETE ON "lenses"."lenses" FOR EACH ROW EXECUTE FUNCTION "content"."sync_lens_count"();



CREATE OR REPLACE TRIGGER "trg_vpc_updated_at" BEFORE UPDATE ON "lenses"."version_parameter_contents" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_workflow_edges_acyclic" AFTER INSERT OR UPDATE ON "lenses"."workflow_edges" FOR EACH ROW EXECUTE FUNCTION "lenses"."trg_workflow_edges_acyclic_fn"();



CREATE OR REPLACE TRIGGER "trg_workflow_phases_updated_at" BEFORE UPDATE ON "lenses"."workflow_phases" FOR EACH ROW EXECUTE FUNCTION "lenses"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_workflow_tasks_updated_at" BEFORE UPDATE ON "lenses"."workflow_tasks" FOR EACH ROW EXECUTE FUNCTION "lenses"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_workflow_version_immutable" BEFORE UPDATE ON "lenses"."workflow_versions" FOR EACH ROW EXECUTE FUNCTION "lenses"."trg_workflow_version_immutable_fn"();



CREATE OR REPLACE TRIGGER "trg_workflows_updated_at" BEFORE UPDATE ON "lenses"."workflows" FOR EACH ROW EXECUTE FUNCTION "lenses"."fn_touch_workflow_updated_at"();



CREATE OR REPLACE TRIGGER "trg_xp_lens_visibility_changed" AFTER UPDATE OF "visibility", "status" ON "lenses"."lenses" FOR EACH ROW EXECUTE FUNCTION "xp"."on_lens_visibility_changed"();

ALTER TABLE "lenses"."lenses" DISABLE TRIGGER "trg_xp_lens_visibility_changed";



CREATE OR REPLACE TRIGGER "trg_xp_workflow_created" AFTER INSERT ON "lenses"."workflows" FOR EACH ROW EXECUTE FUNCTION "xp"."on_workflow_created"();

ALTER TABLE "lenses"."workflows" DISABLE TRIGGER "trg_xp_workflow_created";



CREATE OR REPLACE TRIGGER "trg_xp_workflow_visibility_changed" AFTER UPDATE OF "visibility" ON "lenses"."workflows" FOR EACH ROW EXECUTE FUNCTION "xp"."on_workflow_visibility_changed"();

ALTER TABLE "lenses"."workflows" DISABLE TRIGGER "trg_xp_workflow_visibility_changed";



CREATE OR REPLACE TRIGGER "xp_on_tutorial_completed" AFTER INSERT ON "lenses"."tutorial_completions" FOR EACH ROW EXECUTE FUNCTION "lenses"."fn_xp_on_tutorial_completed"();



COMMENT ON TRIGGER "xp_on_tutorial_completed" ON "lenses"."tutorial_completions" IS 'CB-P1b: Awards TUTORIAL_COMPLETED or WALKTHROUGH_COMPLETED XP on server-side insert.';



CREATE OR REPLACE TRIGGER "xp_on_workflow_run_received" AFTER INSERT ON "lenses"."workflow_runs" FOR EACH ROW EXECUTE FUNCTION "lenses"."fn_xp_on_workflow_run_received"();



COMMENT ON TRIGGER "xp_on_workflow_run_received" ON "lenses"."workflow_runs" IS 'CB-P1a: Awards WORKFLOW_RUN_RECEIVED XP to the workflow owner when another lenser runs their public workflow.';



CREATE OR REPLACE TRIGGER "trg_media_audit_visibility" AFTER UPDATE ON "media"."objects" FOR EACH ROW EXECUTE FUNCTION "media"."trg_fn_audit_visibility"();



COMMENT ON TRIGGER "trg_media_audit_visibility" ON "media"."objects" IS 'AT: Writes audit.events row when visibility changes.';



CREATE OR REPLACE TRIGGER "trg_objects_set_updated_at" BEFORE UPDATE ON "media"."objects" FOR EACH ROW EXECUTE FUNCTION "media"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_addresses_updated_at" BEFORE UPDATE ON "organizations"."addresses" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_audit_logs_no_delete" BEFORE DELETE ON "organizations"."audit_logs" FOR EACH ROW EXECUTE FUNCTION "organizations"."trg_audit_logs_immutable"();



CREATE OR REPLACE TRIGGER "trg_audit_logs_no_update" BEFORE UPDATE ON "organizations"."audit_logs" FOR EACH ROW EXECUTE FUNCTION "organizations"."trg_audit_logs_immutable"();



CREATE OR REPLACE TRIGGER "trg_audit_org_membership" AFTER INSERT OR DELETE OR UPDATE ON "organizations"."members" FOR EACH ROW EXECUTE FUNCTION "organizations"."trg_audit_membership"();



CREATE OR REPLACE TRIGGER "trg_companies_updated_at" BEFORE UPDATE ON "organizations"."companies" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_organizations_create_workspace" AFTER INSERT ON "organizations"."organizations" FOR EACH ROW EXECUTE FUNCTION "organizations"."fn_create_org_workspace"();



CREATE OR REPLACE TRIGGER "trg_organizations_updated_at" BEFORE UPDATE ON "organizations"."organizations" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_preferences_updated_at" BEFORE UPDATE ON "organizations"."preferences" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_guard_notification_fields" BEFORE UPDATE ON "public"."notifications" FOR EACH ROW EXECUTE FUNCTION "public"."guard_notification_fields"();



CREATE OR REPLACE TRIGGER "trg_status_components_touch_updated_at" BEFORE UPDATE ON "status"."components" FOR EACH ROW EXECUTE FUNCTION "status"."touch_updated_at"();



CREATE OR REPLACE TRIGGER "trg_status_incidents_touch_updated_at" BEFORE UPDATE ON "status"."incidents" FOR EACH ROW EXECUTE FUNCTION "status"."touch_updated_at"();



CREATE OR REPLACE TRIGGER "trg_status_maintenance_touch_updated_at" BEFORE UPDATE ON "status"."maintenance_windows" FOR EACH ROW EXECUTE FUNCTION "status"."touch_updated_at"();



CREATE OR REPLACE TRIGGER "trg_status_services_touch_updated_at" BEFORE UPDATE ON "status"."services" FOR EACH ROW EXECUTE FUNCTION "status"."touch_updated_at"();



CREATE OR REPLACE TRIGGER "trg_workspaces_guard_personal" BEFORE DELETE ON "tenancy"."workspaces" FOR EACH ROW EXECUTE FUNCTION "tenancy"."fn_guard_personal_workspace"();



CREATE OR REPLACE TRIGGER "trg_workspaces_guard_personal_status" BEFORE UPDATE ON "tenancy"."workspaces" FOR EACH ROW EXECUTE FUNCTION "tenancy"."fn_guard_personal_workspace_status"();



CREATE OR REPLACE TRIGGER "trg_workspaces_set_updated_at" BEFORE UPDATE ON "tenancy"."workspaces" FOR EACH ROW EXECUTE FUNCTION "tenancy"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_audit_wallet_account" AFTER UPDATE ON "wallet"."accounts" FOR EACH ROW EXECUTE FUNCTION "audit"."trg_fn_audit_wallet_account"();



CREATE OR REPLACE TRIGGER "trg_org_accounts_updated_at" BEFORE UPDATE ON "wallet"."organization_accounts" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_transactions_no_delete" BEFORE DELETE ON "wallet"."transactions" FOR EACH ROW EXECUTE FUNCTION "wallet"."trg_transactions_immutable"();



CREATE OR REPLACE TRIGGER "trg_transactions_no_update" BEFORE UPDATE ON "wallet"."transactions" FOR EACH ROW EXECUTE FUNCTION "wallet"."trg_transactions_immutable"();



CREATE OR REPLACE TRIGGER "trg_verify_balance_after" BEFORE INSERT ON "wallet"."transactions" FOR EACH ROW EXECUTE FUNCTION "wallet"."trg_verify_balance_after"();



CREATE OR REPLACE TRIGGER "trg_wallet_accounts_updated_at" BEFORE UPDATE ON "wallet"."accounts" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_wallet_spending_limits_updated_at" BEFORE UPDATE ON "wallet"."spending_limits" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();



CREATE OR REPLACE TRIGGER "trg_no_delete_events" BEFORE DELETE ON "xp"."events" FOR EACH ROW EXECUTE FUNCTION "xp"."prevent_event_mutations"();



CREATE OR REPLACE TRIGGER "trg_no_update_events" BEFORE UPDATE ON "xp"."events" FOR EACH ROW EXECUTE FUNCTION "xp"."prevent_event_mutations"();



CREATE OR REPLACE TRIGGER "trg_sync_total" AFTER INSERT OR UPDATE ON "xp"."totals" FOR EACH ROW EXECUTE FUNCTION "xp"."sync_total"();



CREATE OR REPLACE TRIGGER "trg_xp_levelup_badge_check" AFTER INSERT ON "xp"."level_ups" FOR EACH ROW EXECUTE FUNCTION "xp"."fn_trigger_badge_check"();



COMMENT ON TRIGGER "trg_xp_levelup_badge_check" ON "xp"."level_ups" IS 'CB: Fires fn_badge_check_and_award after each level-up insert.';



ALTER TABLE ONLY "admin"."execution_control"
    ADD CONSTRAINT "execution_control_frozen_by_fkey" FOREIGN KEY ("frozen_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "admin"."kill_switches"
    ADD CONSTRAINT "kill_switches_operator_id_fkey" FOREIGN KEY ("operator_id") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "agents"."action_logs"
    ADD CONSTRAINT "action_logs_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id");



ALTER TABLE ONLY "agents"."agent_run_events"
    ADD CONSTRAINT "agent_run_events_agent_run_step_id_fkey" FOREIGN KEY ("agent_run_step_id") REFERENCES "agents"."agent_run_steps"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."agent_run_events"
    ADD CONSTRAINT "agent_run_events_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."agent_run_steps"
    ADD CONSTRAINT "agent_run_steps_team_member_id_fkey" FOREIGN KEY ("team_member_id") REFERENCES "agents"."team_members"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."agent_run_steps"
    ADD CONSTRAINT "agent_run_steps_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."agent_run_steps"
    ADD CONSTRAINT "agent_run_steps_workflow_node_id_fkey" FOREIGN KEY ("workflow_node_id") REFERENCES "lenses"."workflow_nodes"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."ai_lensers"
    ADD CONSTRAINT "ai_lensers_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."battle_subscriptions"
    ADD CONSTRAINT "battle_subscriptions_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."battle_subscriptions"
    ADD CONSTRAINT "battle_subscriptions_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id");



ALTER TABLE ONLY "agents"."evaluation_baselines"
    ADD CONSTRAINT "evaluation_baselines_evaluation_id_fkey" FOREIGN KEY ("evaluation_id") REFERENCES "agents"."evaluations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_baselines"
    ADD CONSTRAINT "evaluation_baselines_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "agents"."evaluation_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_baselines"
    ADD CONSTRAINT "evaluation_baselines_set_by_fkey" FOREIGN KEY ("set_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."evaluation_case_results"
    ADD CONSTRAINT "evaluation_case_results_case_id_fkey" FOREIGN KEY ("case_id") REFERENCES "agents"."evaluation_cases"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_case_results"
    ADD CONSTRAINT "evaluation_case_results_evaluation_run_id_fkey" FOREIGN KEY ("evaluation_run_id") REFERENCES "agents"."evaluation_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_cases"
    ADD CONSTRAINT "evaluation_cases_evaluation_id_fkey" FOREIGN KEY ("evaluation_id") REFERENCES "agents"."evaluations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_rubrics"
    ADD CONSTRAINT "evaluation_rubrics_evaluation_id_fkey" FOREIGN KEY ("evaluation_id") REFERENCES "agents"."evaluations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_runs"
    ADD CONSTRAINT "evaluation_runs_evaluation_id_fkey" FOREIGN KEY ("evaluation_id") REFERENCES "agents"."evaluations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."evaluation_runs"
    ADD CONSTRAINT "evaluation_runs_rubric_id_fkey" FOREIGN KEY ("rubric_id") REFERENCES "agents"."evaluation_rubrics"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."evaluations"
    ADD CONSTRAINT "evaluations_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."gateway_commands"
    ADD CONSTRAINT "gateway_commands_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "agents"."gateway_devices"("device_id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."gateway_devices"
    ADD CONSTRAINT "gateway_devices_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."lens_bindings"
    ADD CONSTRAINT "lens_bindings_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."lens_bindings"
    ADD CONSTRAINT "lens_bindings_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id");



ALTER TABLE ONLY "agents"."lens_bindings"
    ADD CONSTRAINT "lens_bindings_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id");



ALTER TABLE ONLY "agents"."memories"
    ADD CONSTRAINT "memories_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."memories"
    ADD CONSTRAINT "memories_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "agents"."memory_profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."memories"
    ADD CONSTRAINT "memories_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."memory_access_logs"
    ADD CONSTRAINT "memory_access_logs_memory_id_fkey" FOREIGN KEY ("memory_id") REFERENCES "agents"."memories"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."memory_access_logs"
    ADD CONSTRAINT "memory_access_logs_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."memory_profiles"
    ADD CONSTRAINT "memory_profiles_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."model_bindings"
    ADD CONSTRAINT "model_bindings_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."model_bindings"
    ADD CONSTRAINT "model_bindings_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id");



ALTER TABLE ONLY "agents"."model_profiles"
    ADD CONSTRAINT "model_profiles_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."model_profiles"
    ADD CONSTRAINT "model_profiles_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."ownerships"
    ADD CONSTRAINT "ownerships_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."ownerships"
    ADD CONSTRAINT "ownerships_owner_lenser_id_fkey" FOREIGN KEY ("owner_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."personality_profiles"
    ADD CONSTRAINT "personality_profiles_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."policies"
    ADD CONSTRAINT "policies_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."policy_evaluations"
    ADD CONSTRAINT "policy_evaluations_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."policy_evaluations"
    ADD CONSTRAINT "policy_evaluations_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."policy_evaluations"
    ADD CONSTRAINT "policy_evaluations_tool_invocation_id_fkey" FOREIGN KEY ("tool_invocation_id") REFERENCES "agents"."tool_invocations"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."provider_configs"
    ADD CONSTRAINT "provider_configs_ai_key_id_fkey" FOREIGN KEY ("ai_key_id") REFERENCES "ai"."keys"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."provider_configs"
    ADD CONSTRAINT "provider_configs_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."quota_snapshots"
    ADD CONSTRAINT "quota_snapshots_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."run_incidents"
    ADD CONSTRAINT "run_incidents_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."run_incidents"
    ADD CONSTRAINT "run_incidents_run_report_id_fkey" FOREIGN KEY ("run_report_id") REFERENCES "agents"."run_reports"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."run_reports"
    ADD CONSTRAINT "run_reports_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."run_reports"
    ADD CONSTRAINT "run_reports_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."run_reports"
    ADD CONSTRAINT "run_reports_workflow_run_id_fkey" FOREIGN KEY ("workflow_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."scratchpad_runs"
    ADD CONSTRAINT "scratchpad_runs_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."standing_approvals"
    ADD CONSTRAINT "standing_approvals_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."standing_approvals"
    ADD CONSTRAINT "standing_approvals_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "agents"."standing_approvals"
    ADD CONSTRAINT "standing_approvals_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_edges"
    ADD CONSTRAINT "team_edges_source_member_id_fkey" FOREIGN KEY ("source_member_id") REFERENCES "agents"."team_members"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_edges"
    ADD CONSTRAINT "team_edges_target_member_id_fkey" FOREIGN KEY ("target_member_id") REFERENCES "agents"."team_members"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_edges"
    ADD CONSTRAINT "team_edges_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "agents"."teams"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_memory_profile_id_fkey" FOREIGN KEY ("memory_profile_id") REFERENCES "agents"."memory_profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_model_profile_id_fkey" FOREIGN KEY ("model_profile_id") REFERENCES "agents"."model_profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_personality_profile_id_fkey" FOREIGN KEY ("personality_profile_id") REFERENCES "agents"."personality_profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "agents"."teams"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_members"
    ADD CONSTRAINT "team_members_tool_profile_id_fkey" FOREIGN KEY ("tool_profile_id") REFERENCES "agents"."tool_profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_messages"
    ADD CONSTRAINT "team_messages_parent_message_id_fkey" FOREIGN KEY ("parent_message_id") REFERENCES "agents"."team_messages"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_messages"
    ADD CONSTRAINT "team_messages_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_parent_team_run_id_fkey" FOREIGN KEY ("parent_team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "agents"."teams"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_workflow_assignment_id_fkey" FOREIGN KEY ("workflow_assignment_id") REFERENCES "agents"."workflow_assignments"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."team_runs"
    ADD CONSTRAINT "team_runs_workflow_run_id_fkey" FOREIGN KEY ("workflow_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."teams"
    ADD CONSTRAINT "teams_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."tool_assignments"
    ADD CONSTRAINT "tool_assignments_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."tool_assignments"
    ADD CONSTRAINT "tool_assignments_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "agents"."tool_profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."tool_assignments"
    ADD CONSTRAINT "tool_assignments_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "agents"."tools_registry"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."tool_invocations"
    ADD CONSTRAINT "tool_invocations_agent_run_step_id_fkey" FOREIGN KEY ("agent_run_step_id") REFERENCES "agents"."agent_run_steps"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."tool_invocations"
    ADD CONSTRAINT "tool_invocations_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."tool_invocations"
    ADD CONSTRAINT "tool_invocations_approval_decided_by_fkey" FOREIGN KEY ("approval_decided_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "agents"."tool_invocations"
    ADD CONSTRAINT "tool_invocations_team_run_id_fkey" FOREIGN KEY ("team_run_id") REFERENCES "agents"."team_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."tool_invocations"
    ADD CONSTRAINT "tool_invocations_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "agents"."tools_registry"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "agents"."tool_profiles"
    ADD CONSTRAINT "tool_profiles_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."workflow_assignments"
    ADD CONSTRAINT "workflow_assignments_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."workflow_assignments"
    ADD CONSTRAINT "workflow_assignments_assignee_ai_lenser_id_fkey" FOREIGN KEY ("assignee_ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."workflow_assignments"
    ADD CONSTRAINT "workflow_assignments_assignee_team_id_fkey" FOREIGN KEY ("assignee_team_id") REFERENCES "agents"."teams"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."workflow_assignments"
    ADD CONSTRAINT "workflow_assignments_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."workspace_settings"
    ADD CONSTRAINT "workspace_settings_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "agents"."workspace_switches"
    ADD CONSTRAINT "workspace_switches_from_ai_lenser_id_fkey" FOREIGN KEY ("from_ai_lenser_id") REFERENCES "agents"."ai_lensers"("id");



ALTER TABLE ONLY "agents"."workspace_switches"
    ADD CONSTRAINT "workspace_switches_human_lenser_id_fkey" FOREIGN KEY ("human_lenser_id") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "agents"."workspace_switches"
    ADD CONSTRAINT "workspace_switches_to_ai_lenser_id_fkey" FOREIGN KEY ("to_ai_lenser_id") REFERENCES "agents"."ai_lensers"("id");



ALTER TABLE ONLY "ai"."key_usage_log"
    ADD CONSTRAINT "key_usage_log_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "ai"."keys"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "ai"."key_usage_log"
    ADD CONSTRAINT "key_usage_log_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "ai"."key_usage_log"
    ADD CONSTRAINT "key_usage_log_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "ai"."key_usage_log"
    ADD CONSTRAINT "key_usage_log_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "ai"."keys"
    ADD CONSTRAINT "keys_lenser_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "ai"."keys"
    ADD CONSTRAINT "keys_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "ai"."providers"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "ai"."modality_pricing"
    ADD CONSTRAINT "modality_pricing_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "ai"."model_pricing"
    ADD CONSTRAINT "model_pricing_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "ai"."models"
    ADD CONSTRAINT "models_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "ai"."providers"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."agent_cost_daily"
    ADD CONSTRAINT "agent_cost_daily_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."contact"
    ADD CONSTRAINT "contact_messages_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."eval_quality_daily"
    ADD CONSTRAINT "eval_quality_daily_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."eval_quality_daily"
    ADD CONSTRAINT "eval_quality_daily_evaluation_id_fkey" FOREIGN KEY ("evaluation_id") REFERENCES "agents"."evaluations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."product_feedback"
    ADD CONSTRAINT "feedback_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."lenser_activity"
    ADD CONSTRAINT "lenser_activity_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."lenser_stats"
    ADD CONSTRAINT "lenser_engagement_totals_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."lenser_join_log"
    ADD CONSTRAINT "lenser_join_log_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."page_views"
    ADD CONSTRAINT "page_views_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."page_views"
    ADD CONSTRAINT "page_views_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."share_events"
    ADD CONSTRAINT "share_events_shared_link_id_fkey" FOREIGN KEY ("shared_link_id") REFERENCES "analytics"."shared_links"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."share_events"
    ADD CONSTRAINT "share_events_viewer_lenser_id_fkey" FOREIGN KEY ("viewer_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."shared_links"
    ADD CONSTRAINT "shared_links_creator_lenser_id_fkey" FOREIGN KEY ("creator_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."tag_activity_events"
    ADD CONSTRAINT "tag_activity_events_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "analytics"."workflow_perf_daily"
    ADD CONSTRAINT "workflow_perf_daily_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "analytics"."workflow_perf_daily"
    ADD CONSTRAINT "workflow_perf_daily_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "audit"."account_lifecycle"
    ADD CONSTRAINT "account_lifecycle_audit_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "audit"."attestations"
    ADD CONSTRAINT "attestations_attested_by_fkey" FOREIGN KEY ("attested_by") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "audit"."byok_key_usage"
    ADD CONSTRAINT "byok_key_usage_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "audit"."byok_key_usage"
    ADD CONSTRAINT "byok_key_usage_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "execution"."byok_keys"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "audit"."disputes"
    ADD CONSTRAINT "disputes_disputer_lenser_id_fkey" FOREIGN KEY ("disputer_lenser_id") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "audit"."disputes"
    ADD CONSTRAINT "disputes_resolved_by_fkey" FOREIGN KEY ("resolved_by") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "audit"."hash_chains"
    ADD CONSTRAINT "hash_chains_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "audit"."events"("id");



ALTER TABLE ONLY "audit"."vote_anomalies"
    ADD CONSTRAINT "vote_anomalies_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "audit"."vote_anomalies"
    ADD CONSTRAINT "vote_anomalies_resolved_by_fkey" FOREIGN KEY ("resolved_by") REFERENCES "auth"."users"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "audit"."vote_anomalies"
    ADD CONSTRAINT "vote_anomalies_voter_lenser_id_fkey" FOREIGN KEY ("voter_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "authz"."developer_tokens"
    ADD CONSTRAINT "developer_tokens_issued_from_request_id_fkey" FOREIGN KEY ("issued_from_request_id") REFERENCES "authz"."device_approval_requests"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "authz"."developer_tokens"
    ADD CONSTRAINT "developer_tokens_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "authz"."device_approval_requests"
    ADD CONSTRAINT "device_approval_requests_approved_by_lenser_id_fkey" FOREIGN KEY ("approved_by_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "authz"."device_approval_requests"
    ADD CONSTRAINT "device_approval_requests_requested_by_lenser_id_fkey" FOREIGN KEY ("requested_by_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "automation"."event_dispatches"
    ADD CONSTRAINT "event_dispatches_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "automation"."events"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "automation"."event_dispatches"
    ADD CONSTRAINT "event_dispatches_rule_id_fkey" FOREIGN KEY ("rule_id") REFERENCES "automation"."trigger_rules"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."ai_handicap_policies"
    ADD CONSTRAINT "ai_handicap_policies_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."ai_judge_verdicts"
    ADD CONSTRAINT "ai_judge_verdicts_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."ai_judge_verdicts"
    ADD CONSTRAINT "ai_judge_verdicts_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."ai_judge_verdicts"
    ADD CONSTRAINT "ai_judge_verdicts_criterion_id_fkey" FOREIGN KEY ("criterion_id") REFERENCES "battles"."rubric_criteria"("id");



ALTER TABLE ONLY "battles"."battle_event_subscriptions"
    ADD CONSTRAINT "battle_event_subscriptions_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."battle_event_subscriptions"
    ADD CONSTRAINT "battle_event_subscriptions_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."battle_execution_dead_letters"
    ADD CONSTRAINT "battle_execution_dead_letters_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "battles"."battle_execution_jobs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."battle_execution_jobs"
    ADD CONSTRAINT "battle_execution_jobs_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."battle_execution_jobs"
    ADD CONSTRAINT "battle_execution_jobs_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_creator_lenser_id_fkey" FOREIGN KEY ("creator_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_forum_thread_id_fkey" FOREIGN KEY ("forum_thread_id") REFERENCES "content"."threads"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_generated_challenge_id_fkey" FOREIGN KEY ("generated_challenge_id") REFERENCES "battles"."generated_challenges"("id");



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_parent_battle_id_fkey" FOREIGN KEY ("parent_battle_id") REFERENCES "battles"."battles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_rubric_id_fkey" FOREIGN KEY ("rubric_id") REFERENCES "battles"."rubrics"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "battles"."templates"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_winner_contender_id_fkey" FOREIGN KEY ("winner_contender_id") REFERENCES "battles"."contenders"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."battles"
    ADD CONSTRAINT "battles_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."comments"
    ADD CONSTRAINT "comments_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."contender_entity_map"
    ADD CONSTRAINT "contender_entity_map_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id");



ALTER TABLE ONLY "battles"."contender_entity_map"
    ADD CONSTRAINT "contender_entity_map_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."contender_entity_map"
    ADD CONSTRAINT "contender_entity_map_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "lensers"."groups"("id");



ALTER TABLE ONLY "battles"."contender_entity_map"
    ADD CONSTRAINT "contender_entity_map_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "battles"."contender_lens_assignments"
    ADD CONSTRAINT "contender_lens_assignments_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."contender_lens_assignments"
    ADD CONSTRAINT "contender_lens_assignments_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."contender_lens_assignments"
    ADD CONSTRAINT "contender_lens_assignments_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id");



ALTER TABLE ONLY "battles"."contender_lens_assignments"
    ADD CONSTRAINT "contender_lens_assignments_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id");



ALTER TABLE ONLY "battles"."contender_runs"
    ADD CONSTRAINT "contender_runs_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."contender_runs"
    ADD CONSTRAINT "contender_runs_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."contender_runs"
    ADD CONSTRAINT "contender_runs_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "battles"."contenders"
    ADD CONSTRAINT "contenders_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."cost_ledgers"
    ADD CONSTRAINT "cost_ledgers_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."cost_ledgers"
    ADD CONSTRAINT "cost_ledgers_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."cost_ledgers"
    ADD CONSTRAINT "cost_ledgers_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."events"
    ADD CONSTRAINT "events_actor_fk" FOREIGN KEY ("actor_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."events"
    ADD CONSTRAINT "events_battle_fk" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."execution_configs"
    ADD CONSTRAINT "execution_configs_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."execution_configs"
    ADD CONSTRAINT "execution_configs_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."execution_configs"
    ADD CONSTRAINT "execution_configs_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id");



ALTER TABLE ONLY "battles"."funding_policies"
    ADD CONSTRAINT "funding_policies_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."funding_policies"
    ADD CONSTRAINT "funding_policies_sponsor_lenser_id_fkey" FOREIGN KEY ("sponsor_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."generated_challenges"
    ADD CONSTRAINT "generated_challenges_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."global_messages"
    ADD CONSTRAINT "global_messages_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."global_messages"
    ADD CONSTRAINT "global_messages_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."invitations"
    ADD CONSTRAINT "invitations_battle_fk" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."invitations"
    ADD CONSTRAINT "invitations_invitee_fk" FOREIGN KEY ("invited_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."invitations"
    ADD CONSTRAINT "invitations_inviter_fk" FOREIGN KEY ("invited_by") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."invite_links"
    ADD CONSTRAINT "invite_links_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."invite_links"
    ADD CONSTRAINT "invite_links_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."media_quality_results"
    ADD CONSTRAINT "media_quality_results_submission_id_fkey" FOREIGN KEY ("submission_id") REFERENCES "battles"."submissions"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."media_quality_rules"
    ADD CONSTRAINT "media_quality_rules_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "battles"."templates"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."model_test_runs"
    ADD CONSTRAINT "model_test_runs_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."model_test_runs"
    ADD CONSTRAINT "model_test_runs_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "battles"."templates"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."rubric_criteria"
    ADD CONSTRAINT "rubric_criteria_rubric_id_fkey" FOREIGN KEY ("rubric_id") REFERENCES "battles"."rubrics"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."rubrics"
    ADD CONSTRAINT "rubrics_creator_lenser_id_fkey" FOREIGN KEY ("creator_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."rule_snapshots"
    ADD CONSTRAINT "rule_snapshots_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."rule_snapshots"
    ADD CONSTRAINT "rule_snapshots_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "battles"."schedules"
    ADD CONSTRAINT "schedules_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."scorecards"
    ADD CONSTRAINT "scorecards_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."scorecards"
    ADD CONSTRAINT "scorecards_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."scorecards"
    ADD CONSTRAINT "scorecards_rubric_criterion_id_fkey" FOREIGN KEY ("rubric_criterion_id") REFERENCES "battles"."rubric_criteria"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."scorecards"
    ADD CONSTRAINT "scorecards_scorer_model_id_fkey" FOREIGN KEY ("scorer_model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."scoring_plugin_signals"
    ADD CONSTRAINT "scoring_plugin_signals_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."series_rounds"
    ADD CONSTRAINT "series_rounds_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."series_rounds"
    ADD CONSTRAINT "series_rounds_series_id_fkey" FOREIGN KEY ("series_id") REFERENCES "battles"."series"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."series_rounds"
    ADD CONSTRAINT "series_rounds_winner_contender_id_fkey" FOREIGN KEY ("winner_contender_id") REFERENCES "battles"."contenders"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."series"
    ADD CONSTRAINT "series_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "battles"."templates"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."sponsorship_contributions"
    ADD CONSTRAINT "sponsorship_contributions_contributor_fkey" FOREIGN KEY ("contributor_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."sponsorship_contributions"
    ADD CONSTRAINT "sponsorship_contributions_pool_id_fkey" FOREIGN KEY ("pool_id") REFERENCES "battles"."sponsorship_pools"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."sponsorship_payouts"
    ADD CONSTRAINT "sponsorship_payouts_pool_id_fkey" FOREIGN KEY ("pool_id") REFERENCES "battles"."sponsorship_pools"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."sponsorship_payouts"
    ADD CONSTRAINT "sponsorship_payouts_recipient_fkey" FOREIGN KEY ("recipient_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."sponsorship_pools"
    ADD CONSTRAINT "sponsorship_pools_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."stream_recordings"
    ADD CONSTRAINT "stream_recordings_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."stream_recordings"
    ADD CONSTRAINT "stream_recordings_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_artifact_id_fkey" FOREIGN KEY ("artifact_id") REFERENCES "execution"."artifacts"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_execution_run_id_fkey" FOREIGN KEY ("execution_run_id") REFERENCES "execution"."runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_revision_of_id_fkey" FOREIGN KEY ("revision_of_id") REFERENCES "battles"."submissions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."submissions"
    ADD CONSTRAINT "submissions_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id");



ALTER TABLE ONLY "battles"."template_prompt_variables"
    ADD CONSTRAINT "template_prompt_variables_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "battles"."templates"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."templates"
    ADD CONSTRAINT "templates_creator_fk" FOREIGN KEY ("creator_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."templates"
    ADD CONSTRAINT "templates_rubric_fk" FOREIGN KEY ("rubric_id") REFERENCES "battles"."rubrics"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."tournament_contenders"
    ADD CONSTRAINT "tournament_contenders_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."tournament_contenders"
    ADD CONSTRAINT "tournament_contenders_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "battles"."tournaments"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id");



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_contender_a_id_fkey" FOREIGN KEY ("contender_a_id") REFERENCES "battles"."tournament_contenders"("id");



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_contender_b_id_fkey" FOREIGN KEY ("contender_b_id") REFERENCES "battles"."tournament_contenders"("id");



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_next_match_id_fkey" FOREIGN KEY ("next_match_id") REFERENCES "battles"."tournament_matches"("id");



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_round_id_fkey" FOREIGN KEY ("round_id") REFERENCES "battles"."tournament_rounds"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "battles"."tournaments"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."tournament_matches"
    ADD CONSTRAINT "tournament_matches_winner_tournament_contender_id_fkey" FOREIGN KEY ("winner_tournament_contender_id") REFERENCES "battles"."tournament_contenders"("id");



ALTER TABLE ONLY "battles"."tournament_rounds"
    ADD CONSTRAINT "tournament_rounds_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "battles"."tournaments"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."tournaments"
    ADD CONSTRAINT "tournaments_creator_lenser_id_fkey" FOREIGN KEY ("creator_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "battles"."vote_aggregates"
    ADD CONSTRAINT "vote_aggregates_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."vote_aggregates"
    ADD CONSTRAINT "vote_aggregates_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."vote_choices"
    ADD CONSTRAINT "vote_choices_contender_id_fkey" FOREIGN KEY ("contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."vote_choices"
    ADD CONSTRAINT "vote_choices_vote_id_fkey" FOREIGN KEY ("vote_id") REFERENCES "battles"."votes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."votes"
    ADD CONSTRAINT "votes_battle_id_fkey" FOREIGN KEY ("battle_id") REFERENCES "battles"."battles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."votes"
    ADD CONSTRAINT "votes_voted_contender_id_fkey" FOREIGN KEY ("voted_contender_id") REFERENCES "battles"."contenders"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "battles"."votes"
    ADD CONSTRAINT "votes_voter_lenser_id_fkey" FOREIGN KEY ("voter_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "billing"."execution_margin_policies"
    ADD CONSTRAINT "billing_margin_policies_model_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "billing"."checkout_sessions"
    ADD CONSTRAINT "checkout_sessions_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "billing"."checkout_sessions"
    ADD CONSTRAINT "checkout_sessions_variant_id_fkey" FOREIGN KEY ("variant_id") REFERENCES "billing"."variants"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "billing"."cost_reservations"
    ADD CONSTRAINT "cost_reservations_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."cost_reservations"
    ADD CONSTRAINT "cost_reservations_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."cost_reservations"
    ADD CONSTRAINT "cost_reservations_pricing_snapshot_id_fkey" FOREIGN KEY ("pricing_snapshot_id") REFERENCES "billing"."pricing_snapshots"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."ledger_entries"
    ADD CONSTRAINT "ledger_entries_reservation_id_fkey" FOREIGN KEY ("reservation_id") REFERENCES "billing"."cost_reservations"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."orders"
    ADD CONSTRAINT "orders_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."orders"
    ADD CONSTRAINT "orders_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "billing"."products"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "billing"."orders"
    ADD CONSTRAINT "orders_variant_id_fkey" FOREIGN KEY ("variant_id") REFERENCES "billing"."variants"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "billing"."orders"
    ADD CONSTRAINT "orders_webhook_event_id_fkey" FOREIGN KEY ("webhook_event_id") REFERENCES "billing"."webhook_events"("id");



ALTER TABLE ONLY "billing"."pricing_snapshots"
    ADD CONSTRAINT "pricing_snapshots_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."pricing_snapshots"
    ADD CONSTRAINT "pricing_snapshots_source_pricing_id_fkey" FOREIGN KEY ("source_pricing_id") REFERENCES "ai"."model_pricing"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "billing"."spend_anomalies"
    ADD CONSTRAINT "spend_anomalies_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "billing"."variants"
    ADD CONSTRAINT "variants_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "billing"."products"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "connectors"."connector_tokens"
    ADD CONSTRAINT "connector_tokens_connector_id_fkey" FOREIGN KEY ("connector_id") REFERENCES "connectors"."connectors"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "connectors"."connectors"
    ADD CONSTRAINT "connectors_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "auth"."users"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "connectors"."connectors"
    ADD CONSTRAINT "connectors_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."entity_translations"
    ADD CONSTRAINT "entity_translations_language_code_fk" FOREIGN KEY ("language_code") REFERENCES "core"."languages"("code") ON DELETE RESTRICT;



ALTER TABLE ONLY "content"."reports"
    ADD CONSTRAINT "reports_reporter_id_fkey" FOREIGN KEY ("reporter_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."tag_map"
    ADD CONSTRAINT "tag_map_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "content"."tags"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."tag_suggestions"
    ADD CONSTRAINT "tag_suggestions_ai_model_id_fkey" FOREIGN KEY ("ai_model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "content"."tag_suggestions"
    ADD CONSTRAINT "tag_suggestions_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "content"."tags"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."tag_translations"
    ADD CONSTRAINT "tag_translations_language_code_fk" FOREIGN KEY ("language_code") REFERENCES "core"."languages"("code") ON DELETE RESTRICT;



ALTER TABLE ONLY "content"."tag_translations"
    ADD CONSTRAINT "tag_translations_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "content"."tags"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."thread_replies"
    ADD CONSTRAINT "thread_replies_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."thread_replies"
    ADD CONSTRAINT "thread_replies_parent_reply_id_fkey" FOREIGN KEY ("parent_reply_id") REFERENCES "content"."thread_replies"("id");



ALTER TABLE ONLY "content"."thread_replies"
    ADD CONSTRAINT "thread_replies_thread_id_fkey" FOREIGN KEY ("thread_id") REFERENCES "content"."threads"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."threads"
    ADD CONSTRAINT "threads_lenser_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "content"."threads"
    ADD CONSTRAINT "threads_linked_lens_id_fkey" FOREIGN KEY ("linked_lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE SET NULL NOT VALID;



ALTER TABLE ONLY "core"."cities"
    ADD CONSTRAINT "cities_country_fk" FOREIGN KEY ("country_id") REFERENCES "core"."countries"("id");



ALTER TABLE ONLY "core"."cities"
    ADD CONSTRAINT "cities_state_fk" FOREIGN KEY ("state_id") REFERENCES "core"."states"("id");



ALTER TABLE ONLY "core"."countries"
    ADD CONSTRAINT "countries_region_fk" FOREIGN KEY ("region_id") REFERENCES "core"."regions"("id");



ALTER TABLE ONLY "core"."countries"
    ADD CONSTRAINT "countries_subregion_fk" FOREIGN KEY ("subregion_id") REFERENCES "core"."subregions"("id");



ALTER TABLE ONLY "core"."states"
    ADD CONSTRAINT "states_country_fk" FOREIGN KEY ("country_id") REFERENCES "core"."countries"("id");



ALTER TABLE ONLY "core"."subregions"
    ADD CONSTRAINT "subregions_region_fk" FOREIGN KEY ("region_id") REFERENCES "core"."regions"("id");



ALTER TABLE ONLY "devices"."device_challenges"
    ADD CONSTRAINT "device_challenges_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."device_challenges"
    ADD CONSTRAINT "device_challenges_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."nonce_cache"
    ADD CONSTRAINT "nonce_cache_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."peer_leases"
    ADD CONSTRAINT "peer_leases_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."peer_leases"
    ADD CONSTRAINT "peer_leases_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."registered_devices"
    ADD CONSTRAINT "registered_devices_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."sync_outbox"
    ADD CONSTRAINT "sync_outbox_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."sync_outbox"
    ADD CONSTRAINT "sync_outbox_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."sync_watermarks"
    ADD CONSTRAINT "sync_watermarks_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "devices"."sync_watermarks"
    ADD CONSTRAINT "sync_watermarks_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."artifact_medias"
    ADD CONSTRAINT "artifact_medias_artifact_fkey" FOREIGN KEY ("artifact_id") REFERENCES "execution"."artifacts"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."artifact_medias"
    ADD CONSTRAINT "artifact_medias_media_fkey" FOREIGN KEY ("media_id") REFERENCES "media"."objects"("id");



ALTER TABLE ONLY "execution"."artifacts"
    ADD CONSTRAINT "artifacts_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."attestation_verifications"
    ADD CONSTRAINT "attestation_verifications_attestation_id_fkey" FOREIGN KEY ("attestation_id") REFERENCES "execution"."attestations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."attestation_verifications"
    ADD CONSTRAINT "attestation_verifications_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."attestations"
    ADD CONSTRAINT "attestations_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."attestations"
    ADD CONSTRAINT "attestations_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."byok_keys"
    ADD CONSTRAINT "byok_keys_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."execution_tags"
    ADD CONSTRAINT "execution_tags_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."links"
    ADD CONSTRAINT "links_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_ai_model_id_fkey" FOREIGN KEY ("ai_model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "execution"."requests"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_version_parameter_id_fkey" FOREIGN KEY ("version_parameter_id") REFERENCES "lenses"."version_parameters"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."parameter_usage_logs"
    ADD CONSTRAINT "parameter_usage_logs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."request_attachments"
    ADD CONSTRAINT "request_attachments_media_object_id_fkey" FOREIGN KEY ("media_object_id") REFERENCES "media"."objects"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "execution"."request_attachments"
    ADD CONSTRAINT "request_attachments_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "execution"."requests"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_byok_key_ref_fkey" FOREIGN KEY ("byok_key_ref_id") REFERENCES "ai"."keys"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE SET NULL NOT VALID;



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_origin_type_fkey" FOREIGN KEY ("origin_type") REFERENCES "execution"."origin_types"("key");



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_requester_lenser_id_fkey" FOREIGN KEY ("requester_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."requests"
    ADD CONSTRAINT "requests_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."runner_device_bindings"
    ADD CONSTRAINT "runner_device_bindings_device_id_fkey" FOREIGN KEY ("device_id") REFERENCES "devices"."registered_devices"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."runner_device_bindings"
    ADD CONSTRAINT "runner_device_bindings_runner_id_fkey" FOREIGN KEY ("runner_id") REFERENCES "execution"."runners"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."runners"
    ADD CONSTRAINT "runners_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."runs"
    ADD CONSTRAINT "runs_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."runs"
    ADD CONSTRAINT "runs_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "execution"."requests"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."steps"
    ADD CONSTRAINT "steps_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "execution"."runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "execution"."steps"
    ADD CONSTRAINT "steps_step_definition_id_fkey" FOREIGN KEY ("step_definition_id") REFERENCES "lenses"."steps"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."stream_sessions"
    ADD CONSTRAINT "stream_sessions_model_key_fkey" FOREIGN KEY ("model_key") REFERENCES "ai"."models"("key") ON DELETE SET NULL NOT VALID;



ALTER TABLE ONLY "execution"."stream_sessions"
    ADD CONSTRAINT "stream_sessions_provider_fkey" FOREIGN KEY ("provider") REFERENCES "ai"."providers"("key") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."stream_sessions"
    ADD CONSTRAINT "stream_sessions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."trust_evaluations"
    ADD CONSTRAINT "trust_evaluations_attestation_id_fkey" FOREIGN KEY ("attestation_id") REFERENCES "execution"."attestations"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "execution"."trust_evaluations"
    ADD CONSTRAINT "trust_evaluations_submission_id_fkey" FOREIGN KEY ("submission_id") REFERENCES "battles"."submissions"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "integrations"."tool_allowlist"
    ADD CONSTRAINT "tool_allowlist_granted_by_fkey" FOREIGN KEY ("granted_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "integrations"."tool_allowlist"
    ADD CONSTRAINT "tool_allowlist_tool_fkey" FOREIGN KEY ("tool_id") REFERENCES "integrations"."tools"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."group_members"
    ADD CONSTRAINT "group_members_group_fkey" FOREIGN KEY ("group_id") REFERENCES "lensers"."groups"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."group_members"
    ADD CONSTRAINT "group_members_lenser_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."groups"
    ADD CONSTRAINT "groups_owner_fkey" FOREIGN KEY ("owner_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "lensers"."badges"
    ADD CONSTRAINT "lenser_badges_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."social_links"
    ADD CONSTRAINT "lenser_social_links_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."profiles"
    ADD CONSTRAINT "lensers_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_active_lenser_id_fkey" FOREIGN KEY ("active_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_ai_model_key_fk" FOREIGN KEY ("ai_model_key") REFERENCES "ai"."models"("key") ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE;



COMMENT ON CONSTRAINT "preferences_ai_model_key_fk" ON "lensers"."preferences" IS 'Preferred AI model must exist in ai.models. NULL = no preferred model. ON DELETE SET NULL when model is removed.';



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_ai_provider_key_fk" FOREIGN KEY ("ai_provider_key") REFERENCES "ai"."providers"("key") ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE;



COMMENT ON CONSTRAINT "preferences_ai_provider_key_fk" ON "lensers"."preferences" IS 'Preferred AI provider must exist in ai.providers. NULL = no preferred provider. ON DELETE SET NULL when provider is removed.';



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_currency_fkey" FOREIGN KEY ("currency") REFERENCES "core"."currencies"("code");



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_language_fk" FOREIGN KEY ("language") REFERENCES "core"."languages"("code") ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE;



COMMENT ON CONSTRAINT "preferences_language_fk" ON "lensers"."preferences" IS 'Language must exist in core.languages. ON UPDATE CASCADE follows code renames. ON DELETE RESTRICT prevents removing a language that is in use.';



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."preferences"
    ADD CONSTRAINT "preferences_selected_api_key_id_fkey" FOREIGN KEY ("selected_api_key_id") REFERENCES "ai"."keys"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lensers"."profiles"
    ADD CONSTRAINT "profiles_ai_model_id_fkey" FOREIGN KEY ("ai_model_id") REFERENCES "ai"."models"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lensers"."relationships"
    ADD CONSTRAINT "relationships_source_profile_id_fkey" FOREIGN KEY ("source_profile_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."relationships"
    ADD CONSTRAINT "relationships_target_profile_id_fkey" FOREIGN KEY ("target_profile_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."tag_follows"
    ADD CONSTRAINT "tag_follows_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lensers"."tag_follows"
    ADD CONSTRAINT "tag_follows_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "content"."tags"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."capability_index"
    ADD CONSTRAINT "capability_index_content_hash_fkey" FOREIGN KEY ("content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."contract_channels"
    ADD CONSTRAINT "contract_channels_content_hash_fkey" FOREIGN KEY ("content_hash") REFERENCES "lenses"."contracts"("content_hash");



ALTER TABLE ONLY "lenses"."contract_channels"
    ADD CONSTRAINT "contract_channels_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."contract_channels"
    ADD CONSTRAINT "contract_channels_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "lenses"."contract_signatures"
    ADD CONSTRAINT "contract_signatures_content_hash_fkey" FOREIGN KEY ("content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."contracts"
    ADD CONSTRAINT "contracts_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."contracts"
    ADD CONSTRAINT "contracts_published_by_fkey" FOREIGN KEY ("published_by") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."contracts"
    ADD CONSTRAINT "contracts_supersedes_hash_fkey" FOREIGN KEY ("supersedes_hash") REFERENCES "lenses"."contracts"("content_hash");



ALTER TABLE ONLY "lenses"."contracts"
    ADD CONSTRAINT "contracts_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."dependency_edges"
    ADD CONSTRAINT "dependency_edges_child_content_hash_fkey" FOREIGN KEY ("child_content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."dependency_edges"
    ADD CONSTRAINT "dependency_edges_parent_content_hash_fkey" FOREIGN KEY ("parent_content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."execution_records"
    ADD CONSTRAINT "execution_records_content_hash_fkey" FOREIGN KEY ("content_hash") REFERENCES "lenses"."contracts"("content_hash");



ALTER TABLE ONLY "lenses"."execution_records"
    ADD CONSTRAINT "execution_records_principal_lenser_id_fkey" FOREIGN KEY ("principal_lenser_id") REFERENCES "lensers"."profiles"("id");



ALTER TABLE ONLY "lenses"."workflow_edges"
    ADD CONSTRAINT "fk_wf_edges_source" FOREIGN KEY ("source_node_id") REFERENCES "lenses"."workflow_nodes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_edges"
    ADD CONSTRAINT "fk_wf_edges_target" FOREIGN KEY ("target_node_id") REFERENCES "lenses"."workflow_nodes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_edges"
    ADD CONSTRAINT "fk_wf_edges_workflow" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_nodes"
    ADD CONSTRAINT "fk_wf_nodes_lens" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_nodes"
    ADD CONSTRAINT "fk_wf_nodes_version" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_nodes"
    ADD CONSTRAINT "fk_wf_nodes_workflow" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "fk_wf_runs_lenser" FOREIGN KEY ("triggered_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "fk_wf_runs_workflow" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "fk_wnr_execution_run" FOREIGN KEY ("execution_run_id") REFERENCES "execution"."runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "fk_wnr_node" FOREIGN KEY ("node_id") REFERENCES "lenses"."workflow_nodes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "fk_wnr_run" FOREIGN KEY ("run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflows"
    ADD CONSTRAINT "fk_workflows_lenser" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."steps"
    ADD CONSTRAINT "lens_steps_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."steps"
    ADD CONSTRAINT "lens_steps_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "ai"."models"("id");



ALTER TABLE ONLY "lenses"."steps"
    ADD CONSTRAINT "lens_steps_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."version_parameters"
    ADD CONSTRAINT "lens_version_params_version_fkey" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."versions"
    ADD CONSTRAINT "lens_versions_lens_fkey" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."versions"
    ADD CONSTRAINT "lens_versions_parent_fkey" FOREIGN KEY ("parent_version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."lenses"
    ADD CONSTRAINT "lenses_forked_from_execution_id_fkey" FOREIGN KEY ("forked_from_execution_id") REFERENCES "execution"."runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."lenses"
    ADD CONSTRAINT "lenses_forked_from_version_id_fkey" FOREIGN KEY ("forked_from_version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."lenses"
    ADD CONSTRAINT "lenses_head_version_id_fkey" FOREIGN KEY ("head_version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."lenses"
    ADD CONSTRAINT "lenses_lenser_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."lenses"
    ADD CONSTRAINT "lenses_parent_lens_id_fkey" FOREIGN KEY ("parent_lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."parameter_contracts"
    ADD CONSTRAINT "parameter_contracts_content_hash_fkey" FOREIGN KEY ("content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."parameter_contracts"
    ADD CONSTRAINT "parameter_contracts_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "lenses"."tools"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."parameter_deprecations"
    ADD CONSTRAINT "parameter_deprecations_from_content_hash_fkey" FOREIGN KEY ("from_content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."parameter_deprecations"
    ADD CONSTRAINT "parameter_deprecations_to_content_hash_fkey" FOREIGN KEY ("to_content_hash") REFERENCES "lenses"."contracts"("content_hash");



ALTER TABLE ONLY "lenses"."security_scopes"
    ADD CONSTRAINT "security_scopes_content_hash_fkey" FOREIGN KEY ("content_hash") REFERENCES "lenses"."contracts"("content_hash") ON DELETE RESTRICT;



ALTER TABLE ONLY "lenses"."steps"
    ADD CONSTRAINT "steps_sub_lens_id_fkey" FOREIGN KEY ("sub_lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."tutorial_completions"
    ADD CONSTRAINT "tutorial_completions_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."version_parameter_contents"
    ADD CONSTRAINT "version_parameter_contents_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."version_parameter_contents"
    ADD CONSTRAINT "version_parameter_contents_parameter_id_fkey" FOREIGN KEY ("parameter_id") REFERENCES "lenses"."version_parameters"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."version_parameter_contents"
    ADD CONSTRAINT "version_parameter_contents_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."version_parameters"
    ADD CONSTRAINT "version_parameters_tool_id_fkey" FOREIGN KEY ("tool_id") REFERENCES "lenses"."tools"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "workflow_node_results_resolved_lens_version_id_fkey" FOREIGN KEY ("resolved_lens_version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_node_results"
    ADD CONSTRAINT "workflow_node_results_workflow_version_node_id_fkey" FOREIGN KEY ("workflow_version_node_id") REFERENCES "lenses"."workflow_version_nodes"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_phases"
    ADD CONSTRAINT "workflow_phases_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_chains"
    ADD CONSTRAINT "workflow_run_chains_child_run_id_fkey" FOREIGN KEY ("child_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_chains"
    ADD CONSTRAINT "workflow_run_chains_parent_run_id_fkey" FOREIGN KEY ("parent_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_dead_letters"
    ADD CONSTRAINT "workflow_run_dead_letters_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_events"
    ADD CONSTRAINT "workflow_run_events_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_provenance"
    ADD CONSTRAINT "workflow_run_provenance_source_run_id_fkey" FOREIGN KEY ("source_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_provenance"
    ADD CONSTRAINT "workflow_run_provenance_target_run_id_fkey" FOREIGN KEY ("target_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_run_tags"
    ADD CONSTRAINT "workflow_run_tags_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "workflow_runs_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id");



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "workflow_runs_parent_run_id_fkey" FOREIGN KEY ("parent_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "workflow_runs_schedule_id_fkey" FOREIGN KEY ("schedule_id") REFERENCES "lenses"."workflow_schedules"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_runs"
    ADD CONSTRAINT "workflow_runs_workflow_version_id_fkey" FOREIGN KEY ("workflow_version_id") REFERENCES "lenses"."workflow_versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_schedules"
    ADD CONSTRAINT "workflow_schedules_calendar_id_fkey" FOREIGN KEY ("calendar_id") REFERENCES "lenses"."schedule_calendars"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_schedules"
    ADD CONSTRAINT "workflow_schedules_last_run_id_fkey" FOREIGN KEY ("last_run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_schedules"
    ADD CONSTRAINT "workflow_schedules_workflow_assignment_id_fkey" FOREIGN KEY ("workflow_assignment_id") REFERENCES "agents"."workflow_assignments"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_schedules"
    ADD CONSTRAINT "workflow_schedules_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_tasks"
    ADD CONSTRAINT "workflow_tasks_phase_id_fkey" FOREIGN KEY ("phase_id") REFERENCES "lenses"."workflow_phases"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_tasks"
    ADD CONSTRAINT "workflow_tasks_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_triggers"
    ADD CONSTRAINT "workflow_triggers_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_triggers"
    ADD CONSTRAINT "workflow_triggers_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_versions"
    ADD CONSTRAINT "workflow_versions_fk_created_by" FOREIGN KEY ("created_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_versions"
    ADD CONSTRAINT "workflow_versions_fk_workflow" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflows"
    ADD CONSTRAINT "workflows_fk_head_version" FOREIGN KEY ("head_version_id") REFERENCES "lenses"."workflow_versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflows"
    ADD CONSTRAINT "workflows_parent_workflow_id_fkey" FOREIGN KEY ("parent_workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_version_edges"
    ADD CONSTRAINT "wve_fk_source" FOREIGN KEY ("source_node_id") REFERENCES "lenses"."workflow_version_nodes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_version_edges"
    ADD CONSTRAINT "wve_fk_target" FOREIGN KEY ("target_node_id") REFERENCES "lenses"."workflow_version_nodes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_version_edges"
    ADD CONSTRAINT "wve_fk_version" FOREIGN KEY ("workflow_version_id") REFERENCES "lenses"."workflow_versions"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_version_nodes"
    ADD CONSTRAINT "wvn_fk_lens" FOREIGN KEY ("lens_id") REFERENCES "lenses"."lenses"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "lenses"."workflow_version_nodes"
    ADD CONSTRAINT "wvn_fk_lens_version" FOREIGN KEY ("version_id") REFERENCES "lenses"."versions"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "lenses"."workflow_version_nodes"
    ADD CONSTRAINT "wvn_fk_version" FOREIGN KEY ("workflow_version_id") REFERENCES "lenses"."workflow_versions"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "media"."attachments"
    ADD CONSTRAINT "attachments_attached_by_fkey" FOREIGN KEY ("attached_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "media"."attachments"
    ADD CONSTRAINT "attachments_object_fkey" FOREIGN KEY ("object_id") REFERENCES "media"."objects"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "media"."objects"
    ADD CONSTRAINT "media_objects_request_id_fkey" FOREIGN KEY ("request_id") REFERENCES "execution"."requests"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "media"."objects"
    ADD CONSTRAINT "objects_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "media"."objects"
    ADD CONSTRAINT "objects_owner_fkey" FOREIGN KEY ("owner_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "media"."objects"
    ADD CONSTRAINT "objects_workspace_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id");



ALTER TABLE ONLY "organizations"."activity_log"
    ADD CONSTRAINT "activity_log_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."activity_log"
    ADD CONSTRAINT "activity_log_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "organizations"."addresses"
    ADD CONSTRAINT "addresses_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "core"."cities"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."addresses"
    ADD CONSTRAINT "addresses_company_id_fkey" FOREIGN KEY ("company_id") REFERENCES "organizations"."companies"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "organizations"."addresses"
    ADD CONSTRAINT "addresses_country_iso2_code_fkey" FOREIGN KEY ("country_iso2_code") REFERENCES "core"."countries"("iso2") ON DELETE RESTRICT;



ALTER TABLE ONLY "organizations"."addresses"
    ADD CONSTRAINT "addresses_state_id_fkey" FOREIGN KEY ("state_id") REFERENCES "core"."states"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."audit_logs"
    ADD CONSTRAINT "audit_logs_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."audit_logs"
    ADD CONSTRAINT "audit_logs_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "organizations"."companies"
    ADD CONSTRAINT "companies_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "organizations"."companies"
    ADD CONSTRAINT "companies_tax_iso2_code_fkey" FOREIGN KEY ("tax_iso2_code") REFERENCES "core"."countries"("iso2");



ALTER TABLE ONLY "organizations"."members"
    ADD CONSTRAINT "members_invited_by_fkey" FOREIGN KEY ("invited_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."members"
    ADD CONSTRAINT "members_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "organizations"."members"
    ADD CONSTRAINT "members_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "organizations"."organizations"
    ADD CONSTRAINT "organizations_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "organizations"."organizations"
    ADD CONSTRAINT "organizations_kill_switched_by_fkey" FOREIGN KEY ("kill_switched_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."organizations"
    ADD CONSTRAINT "organizations_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "organizations"."preferences"
    ADD CONSTRAINT "preferences_iso2_code_fkey" FOREIGN KEY ("iso2_code") REFERENCES "core"."countries"("iso2");



ALTER TABLE ONLY "organizations"."preferences"
    ADD CONSTRAINT "preferences_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "platform"."system_flags"
    ADD CONSTRAINT "system_flags_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "platform"."tool_invocation_logs"
    ADD CONSTRAINT "tool_invocation_logs_ai_lenser_id_fkey" FOREIGN KEY ("ai_lenser_id") REFERENCES "agents"."ai_lensers"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "platform"."tool_invocation_logs"
    ADD CONSTRAINT "tool_invocation_logs_decided_by_fkey" FOREIGN KEY ("decided_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "platform"."tool_invocation_logs"
    ADD CONSTRAINT "tool_invocation_logs_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "lenses"."workflow_runs"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_notification_id_fkey" FOREIGN KEY ("notification_id") REFERENCES "public"."notifications"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_recipient_id_fkey" FOREIGN KEY ("recipient_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."notification_preferences"
    ADD CONSTRAINT "notification_preferences_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."notifications"
    ADD CONSTRAINT "notifications_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "user_oauth_connections_lenser_fk" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "user_oauth_connections_workspace_fk" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."workflow_integration_credentials"
    ADD CONSTRAINT "workflow_integration_credentials_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."workflow_kv_store"
    ADD CONSTRAINT "workflow_kv_store_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."workflow_webhook_triggers"
    ADD CONSTRAINT "workflow_webhook_triggers_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "reputation"."contender_ratings"
    ADD CONSTRAINT "contender_ratings_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "reputation"."judge_calibrations"
    ADD CONSTRAINT "judge_calibrations_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "reputation"."lenser_scores"
    ADD CONSTRAINT "lenser_scores_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "reputation"."vote_risk_scores"
    ADD CONSTRAINT "vote_risk_scores_vote_id_fkey" FOREIGN KEY ("vote_id") REFERENCES "battles"."votes"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "status"."components"
    ADD CONSTRAINT "components_service_id_fkey" FOREIGN KEY ("service_id") REFERENCES "status"."services"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "status"."incident_updates"
    ADD CONSTRAINT "incident_updates_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "status"."incidents"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "status"."incidents"
    ADD CONSTRAINT "incidents_service_id_fkey" FOREIGN KEY ("service_id") REFERENCES "status"."services"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "status"."maintenance_windows"
    ADD CONSTRAINT "maintenance_windows_service_id_fkey" FOREIGN KEY ("service_id") REFERENCES "status"."services"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "tenancy"."workspace_members"
    ADD CONSTRAINT "workspace_members_invited_by_fkey" FOREIGN KEY ("invited_by") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "tenancy"."workspace_members"
    ADD CONSTRAINT "workspace_members_lenser_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "tenancy"."workspace_members"
    ADD CONSTRAINT "workspace_members_workspace_fkey" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "tenancy"."workspaces"
    ADD CONSTRAINT "workspaces_org_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "tenancy"."workspaces"
    ADD CONSTRAINT "workspaces_owner_fkey" FOREIGN KEY ("owner_lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "wallet"."accounts"
    ADD CONSTRAINT "accounts_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "wallet"."organization_accounts"
    ADD CONSTRAINT "organization_accounts_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"."organizations"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "wallet"."pending_charges"
    ADD CONSTRAINT "pending_charges_account_fkey" FOREIGN KEY ("account_id") REFERENCES "wallet"."accounts"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "wallet"."pending_charges"
    ADD CONSTRAINT "pending_charges_lenser_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "wallet"."spending_limits"
    ADD CONSTRAINT "spending_limits_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "wallet"."accounts"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "wallet"."transactions"
    ADD CONSTRAINT "transactions_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "wallet"."accounts"("id") ON DELETE RESTRICT;



ALTER TABLE ONLY "xp"."contributions"
    ADD CONSTRAINT "contributions_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."contributions"
    ADD CONSTRAINT "contributions_xp_event_id_fkey" FOREIGN KEY ("xp_event_id") REFERENCES "xp"."events"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "xp"."events"
    ADD CONSTRAINT "events_season_id_fkey" FOREIGN KEY ("season_id") REFERENCES "xp"."seasons"("id");



ALTER TABLE ONLY "xp"."level_ups"
    ADD CONSTRAINT "lenser_level_ups_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."event_verifications"
    ADD CONSTRAINT "xp_event_verifications_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "xp"."events"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."events"
    ADD CONSTRAINT "xp_events_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "xp"."events"
    ADD CONSTRAINT "xp_events_rule_id_fkey" FOREIGN KEY ("rule_id") REFERENCES "xp"."rules"("id");



ALTER TABLE ONLY "xp"."monthly_rollup"
    ADD CONSTRAINT "xp_monthly_rollup_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."season_totals"
    ADD CONSTRAINT "xp_season_totals_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."season_totals"
    ADD CONSTRAINT "xp_season_totals_season_id_fkey" FOREIGN KEY ("season_id") REFERENCES "xp"."seasons"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."streaks"
    ADD CONSTRAINT "xp_streaks_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "xp"."totals"
    ADD CONSTRAINT "xp_totals_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE "admin"."execution_control" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "admin"."kill_switches" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "kill_switches_deny_all" ON "admin"."kill_switches" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



ALTER TABLE "agents"."action_logs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "action_logs_owner_read" ON "agents"."action_logs" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "agents"."ownerships" "o"
  WHERE (("o"."ai_lenser_id" = "action_logs"."ai_lenser_id") AND ("o"."owner_lenser_id" = "lensers"."get_auth_human_lenser_id"()) AND ("o"."revoked_at" IS NULL)))));



CREATE POLICY "action_logs_service_insert" ON "agents"."action_logs" FOR INSERT TO "service_role" WITH CHECK (true);



ALTER TABLE "agents"."agent_run_events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "agent_run_events_owner_all" ON "agents"."agent_run_events" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "agents"."team_runs" "tr"
  WHERE (("tr"."id" = "agent_run_events"."team_run_id") AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id"))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."team_runs" "tr"
  WHERE (("tr"."id" = "agent_run_events"."team_run_id") AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id")))));



ALTER TABLE "agents"."agent_run_steps" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "agent_run_steps_owner_all" ON "agents"."agent_run_steps" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "agents"."team_runs" "tr"
  WHERE (("tr"."id" = "agent_run_steps"."team_run_id") AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id"))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."team_runs" "tr"
  WHERE (("tr"."id" = "agent_run_steps"."team_run_id") AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id")))));



ALTER TABLE "agents"."ai_lensers" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "ai_lensers_public_read" ON "agents"."ai_lensers" FOR SELECT USING ((("is_active" = true) AND ("suspended_at" IS NULL) AND ("archived_at" IS NULL) AND ("deleted_at" IS NULL)));



CREATE POLICY "ai_lensers_service_write" ON "agents"."ai_lensers" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "agents"."battle_subscriptions" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "agents"."evaluation_baselines" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "evaluation_baselines_owner_all" ON "agents"."evaluation_baselines" USING ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_baselines"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id"))))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_baselines"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id")))))));



ALTER TABLE "agents"."evaluation_case_results" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "evaluation_case_results_owner_all" ON "agents"."evaluation_case_results" USING ((EXISTS ( SELECT 1
   FROM ("agents"."evaluation_runs" "r"
     JOIN "agents"."evaluations" "e" ON (("e"."id" = "r"."evaluation_id")))
  WHERE (("r"."id" = "evaluation_case_results"."evaluation_run_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id"))))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM ("agents"."evaluation_runs" "r"
     JOIN "agents"."evaluations" "e" ON (("e"."id" = "r"."evaluation_id")))
  WHERE (("r"."id" = "evaluation_case_results"."evaluation_run_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id")))))));



ALTER TABLE "agents"."evaluation_cases" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "evaluation_cases_owner_all" ON "agents"."evaluation_cases" USING ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_cases"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id"))))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_cases"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id")))))));



ALTER TABLE "agents"."evaluation_rubrics" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "evaluation_rubrics_owner_all" ON "agents"."evaluation_rubrics" USING ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_rubrics"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id"))))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_rubrics"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id")))))));



ALTER TABLE "agents"."evaluation_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "evaluation_runs_owner_all" ON "agents"."evaluation_runs" USING ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_runs"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id"))))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."evaluations" "e"
  WHERE (("e"."id" = "evaluation_runs"."evaluation_id") AND (("e"."owner_lenser_id" = "auth"."uid"()) OR (("e"."ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("e"."ai_lenser_id")))))));



ALTER TABLE "agents"."evaluations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "evaluations_owner_all" ON "agents"."evaluations" USING ((("owner_lenser_id" = "auth"."uid"()) OR (("ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("ai_lenser_id")))) WITH CHECK ((("owner_lenser_id" = "auth"."uid"()) OR (("ai_lenser_id" IS NOT NULL) AND "agents"."can_manage_ai_lenser"("ai_lenser_id"))));



ALTER TABLE "agents"."gateway_commands" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "gateway_commands_owner_select" ON "agents"."gateway_commands" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "agents"."gateway_devices" "d"
  WHERE (("d"."device_id" = "gateway_commands"."device_id") AND ("d"."owner_id" = "auth"."uid"())))));



ALTER TABLE "agents"."gateway_devices" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "gateway_devices_owner_select" ON "agents"."gateway_devices" FOR SELECT USING (("owner_id" = "auth"."uid"()));



ALTER TABLE "agents"."lens_bindings" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "lens_bindings_public_read" ON "agents"."lens_bindings" FOR SELECT USING (true);



CREATE POLICY "lens_bindings_service_write" ON "agents"."lens_bindings" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "agents"."memories" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "memories_owner_all" ON "agents"."memories" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."memory_access_logs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "memory_access_logs_owner_all" ON "agents"."memory_access_logs" USING ((EXISTS ( SELECT 1
   FROM "agents"."memories" "m"
  WHERE (("m"."id" = "memory_access_logs"."memory_id") AND "agents"."can_manage_ai_lenser"("m"."ai_lenser_id"))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."memories" "m"
  WHERE (("m"."id" = "memory_access_logs"."memory_id") AND "agents"."can_manage_ai_lenser"("m"."ai_lenser_id")))));



ALTER TABLE "agents"."memory_profiles" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "memory_profiles_owner_all" ON "agents"."memory_profiles" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."model_bindings" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "model_bindings_public_read" ON "agents"."model_bindings" FOR SELECT USING (true);



CREATE POLICY "model_bindings_service_write" ON "agents"."model_bindings" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "agents"."model_profiles" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "model_profiles_owner_all" ON "agents"."model_profiles" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."ownerships" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "ownerships_read_own" ON "agents"."ownerships" FOR SELECT USING (("owner_lenser_id" = "lensers"."get_auth_human_lenser_id"()));



CREATE POLICY "ownerships_service_write" ON "agents"."ownerships" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "agents"."personality_profiles" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "personality_profiles_owner_all" ON "agents"."personality_profiles" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."policies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "policies_public_read" ON "agents"."policies" FOR SELECT USING (("is_public_policy" = true));



CREATE POLICY "policies_service_write" ON "agents"."policies" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "agents"."policy_evaluations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "policy_evaluations_owner_insert" ON "agents"."policy_evaluations" FOR INSERT WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



CREATE POLICY "policy_evaluations_owner_select" ON "agents"."policy_evaluations" FOR SELECT USING ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."provider_configs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "provider_configs_owner_all" ON "agents"."provider_configs" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."quota_snapshots" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "quota_snapshots_owner_read" ON "agents"."quota_snapshots" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "agents"."ownerships" "o"
  WHERE (("o"."ai_lenser_id" = "quota_snapshots"."ai_lenser_id") AND ("o"."owner_lenser_id" = "lensers"."get_auth_human_lenser_id"()) AND ("o"."revoked_at" IS NULL)))));



CREATE POLICY "quota_snapshots_service_write" ON "agents"."quota_snapshots" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "agents"."run_incidents" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "run_incidents_owner_all" ON "agents"."run_incidents" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."run_reports" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "run_reports_owner_insert" ON "agents"."run_reports" FOR INSERT WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



CREATE POLICY "run_reports_owner_select" ON "agents"."run_reports" FOR SELECT USING ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."scratchpad_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "scratchpad_runs_owner_select" ON "agents"."scratchpad_runs" FOR SELECT USING ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



CREATE POLICY "scratchpad_runs_owner_write" ON "agents"."scratchpad_runs" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."standing_approvals" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "standing_approvals_owner_all" ON "agents"."standing_approvals" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



CREATE POLICY "sub_owner_all" ON "agents"."battle_subscriptions" USING ((EXISTS ( SELECT 1
   FROM ("agents"."ai_lensers" "al"
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "al"."profile_id")))
  WHERE (("al"."id" = "battle_subscriptions"."agent_id") AND ("p"."user_id" = "auth"."uid"())))));



ALTER TABLE "agents"."team_edges" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "team_edges_owner_all" ON "agents"."team_edges" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "agents"."teams" "t"
  WHERE (("t"."id" = "team_edges"."team_id") AND "agents"."can_manage_ai_lenser"("t"."ai_lenser_id"))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."teams" "t"
  WHERE (("t"."id" = "team_edges"."team_id") AND "agents"."can_manage_ai_lenser"("t"."ai_lenser_id")))));



ALTER TABLE "agents"."team_members" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "team_members_owner_all" ON "agents"."team_members" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "agents"."teams" "t"
  WHERE (("t"."id" = "team_members"."team_id") AND "agents"."can_manage_ai_lenser"("t"."ai_lenser_id"))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "agents"."teams" "t"
  WHERE (("t"."id" = "team_members"."team_id") AND "agents"."can_manage_ai_lenser"("t"."ai_lenser_id")))));



ALTER TABLE "agents"."team_messages" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "team_messages_owner_insert" ON "agents"."team_messages" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("agents"."team_runs" "tr"
     JOIN "agents"."team_members" "tm" ON ((("tm"."team_id" = "tr"."team_id") AND ("tm"."agent_id" = "team_messages"."from_agent_id"))))
  WHERE (("tr"."id" = "team_messages"."team_run_id") AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id")))));



COMMENT ON POLICY "team_messages_owner_insert" ON "agents"."team_messages" IS 'Phase X1 (hardened): owner may INSERT only if from_agent_id is a member of the team backing the team_run. Prevents forged sender attribution.';



CREATE POLICY "team_messages_owner_select" ON "agents"."team_messages" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "agents"."team_runs" "tr"
  WHERE (("tr"."id" = "team_messages"."team_run_id") AND "agents"."can_manage_ai_lenser"("tr"."ai_lenser_id")))));



ALTER TABLE "agents"."team_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "team_runs_owner_all" ON "agents"."team_runs" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."teams" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "teams_owner_all" ON "agents"."teams" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."tool_assignments" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tool_assignments_owner_all" ON "agents"."tool_assignments" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."tool_invocations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tool_invocations_owner_all" ON "agents"."tool_invocations" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."tool_profiles" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tool_profiles_owner_all" ON "agents"."tool_profiles" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."tools_registry" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tools_registry_owner_all" ON "agents"."tools_registry" USING (("owner_lenser_id" = "lensers"."get_auth_human_lenser_id"())) WITH CHECK (("owner_lenser_id" = "lensers"."get_auth_human_lenser_id"()));



ALTER TABLE "agents"."workflow_assignments" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_assignments_owner_all" ON "agents"."workflow_assignments" TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."workspace_settings" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workspace_settings_owner_all" ON "agents"."workspace_settings" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id")) WITH CHECK ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "agents"."workspace_switches" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "ws_insert_own" ON "agents"."workspace_switches" FOR INSERT WITH CHECK (("human_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "ws_select_own" ON "agents"."workspace_switches" FOR SELECT USING (("human_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "ai_models_delete_admin_only" ON "ai"."models" FOR DELETE TO "authenticated" USING ("public"."is_admin"());



CREATE POLICY "ai_models_insert_admin_only" ON "ai"."models" FOR INSERT TO "authenticated" WITH CHECK ("public"."is_admin"());



CREATE POLICY "ai_models_select" ON "ai"."models" FOR SELECT USING ((("is_active" = true) OR "public"."is_admin"()));



CREATE POLICY "ai_models_update_admin_only" ON "ai"."models" FOR UPDATE TO "authenticated" USING ("public"."is_admin"()) WITH CHECK ("public"."is_admin"());



CREATE POLICY "ai_providers_select" ON "ai"."providers" FOR SELECT USING ((("is_active" = true) OR "public"."is_admin"()));



ALTER TABLE "ai"."key_usage_log" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "key_usage_log_select_own" ON "ai"."key_usage_log" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "key_usage_log_service_all" ON "ai"."key_usage_log" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "ai"."keys" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "keys_select_own_fallback" ON "ai"."keys" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "ai"."model_pricing" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "model_pricing_select_active" ON "ai"."model_pricing" FOR SELECT TO "authenticated", "anon" USING (("effective_to" IS NULL));



CREATE POLICY "model_pricing_service_all" ON "ai"."model_pricing" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "ai"."models" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "ai"."providers" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "providers_service_all" ON "ai"."providers" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role_all" ON "ai"."keys" TO "service_role" USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "acd_owner_read" ON "analytics"."agent_cost_daily" FOR SELECT TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "analytics"."agent_cost_daily" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "allow_system_read_lenser_activity" ON "analytics"."lenser_activity" FOR SELECT TO "service_role" USING (true);



CREATE POLICY "auth_user_can_select_own_feedback" ON "analytics"."product_feedback" FOR SELECT TO "authenticated" USING (("user_id" = ( SELECT "auth"."uid"() AS "uid")));



CREATE POLICY "authenticated_can_read_engagement" ON "analytics"."lenser_stats" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "deny_delete_join_log" ON "analytics"."lenser_join_log" FOR DELETE USING (false);



CREATE POLICY "deny_insert_join_log" ON "analytics"."lenser_join_log" FOR INSERT WITH CHECK (false);



CREATE POLICY "deny_update_join_log" ON "analytics"."lenser_join_log" FOR UPDATE USING (false) WITH CHECK (false);



CREATE POLICY "eqd_owner_read" ON "analytics"."eval_quality_daily" FOR SELECT TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "analytics"."eval_quality_daily" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "events_authenticated_select" ON "analytics"."tag_activity_events" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "events_deny_delete" ON "analytics"."tag_activity_events" FOR DELETE USING (false);



CREATE POLICY "events_deny_update" ON "analytics"."tag_activity_events" FOR UPDATE USING (false) WITH CHECK (false);



CREATE POLICY "events_insert_deny" ON "analytics"."tag_activity_events" FOR INSERT WITH CHECK (false);



CREATE POLICY "events_insert_service" ON "analytics"."tag_activity_events" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "join_log_authenticated_select" ON "analytics"."lenser_join_log" FOR SELECT TO "authenticated" USING (true);



ALTER TABLE "analytics"."lenser_activity" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "lenser_activity_service_insert" ON "analytics"."lenser_activity" FOR INSERT WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



ALTER TABLE "analytics"."lenser_join_log" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "analytics"."lenser_stats" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "p_feedback_insert_public" ON "analytics"."product_feedback" FOR INSERT TO "authenticated", "anon" WITH CHECK ((("message" IS NULL) OR ("char_length"("btrim"("message")) >= 10)));



ALTER TABLE "analytics"."page_views" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "page_views_insert_secure" ON "analytics"."page_views" FOR INSERT TO "authenticated", "anon" WITH CHECK ((("path" IS NOT NULL) AND ("length"("path") > 0)));



CREATE POLICY "page_views_no_delete" ON "analytics"."page_views" FOR DELETE USING (false);



CREATE POLICY "page_views_no_select" ON "analytics"."page_views" FOR SELECT USING (false);



CREATE POLICY "page_views_no_update" ON "analytics"."page_views" FOR UPDATE USING (false) WITH CHECK (false);



CREATE POLICY "page_views_select_service_role" ON "analytics"."page_views" FOR SELECT TO "service_role" USING (true);



ALTER TABLE "analytics"."product_feedback" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "service_all_activity" ON "analytics"."lenser_activity" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role_full_access_feedback" ON "analytics"."product_feedback" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "analytics"."share_events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "share_events_select_by_link_owner" ON "analytics"."share_events" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM ("analytics"."shared_links" "sl"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "sl"."creator_lenser_id")))
  WHERE (("sl"."id" = "share_events"."shared_link_id") AND ("l"."user_id" = ( SELECT "auth"."uid"() AS "uid"))))));



ALTER TABLE "analytics"."shared_links" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "shared_links_delete_own" ON "analytics"."shared_links" FOR DELETE USING ((EXISTS ( SELECT 1
   FROM "lensers"."profiles" "l"
  WHERE (("l"."id" = "shared_links"."creator_lenser_id") AND ("l"."user_id" = ( SELECT "auth"."uid"() AS "uid"))))));



CREATE POLICY "shared_links_insert_own" ON "analytics"."shared_links" FOR INSERT WITH CHECK ((EXISTS ( SELECT 1
   FROM "lensers"."profiles" "l"
  WHERE (("l"."id" = "shared_links"."creator_lenser_id") AND ("l"."user_id" = ( SELECT "auth"."uid"() AS "uid"))))));



CREATE POLICY "shared_links_select_own" ON "analytics"."shared_links" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "lensers"."profiles" "l"
  WHERE (("l"."id" = "shared_links"."creator_lenser_id") AND ("l"."user_id" = ( SELECT "auth"."uid"() AS "uid"))))));



CREATE POLICY "shared_links_update_own" ON "analytics"."shared_links" FOR UPDATE USING ((EXISTS ( SELECT 1
   FROM "lensers"."profiles" "l"
  WHERE (("l"."id" = "shared_links"."creator_lenser_id") AND ("l"."user_id" = ( SELECT "auth"."uid"() AS "uid")))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "lensers"."profiles" "l"
  WHERE (("l"."id" = "shared_links"."creator_lenser_id") AND ("l"."user_id" = ( SELECT "auth"."uid"() AS "uid"))))));



ALTER TABLE "analytics"."tag_activity_events" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "analytics"."workflow_perf_daily" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "wpd_owner_read" ON "analytics"."workflow_perf_daily" FOR SELECT TO "authenticated" USING ("agents"."can_manage_ai_lenser"("ai_lenser_id"));



ALTER TABLE "audit"."account_lifecycle" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "audit"."admin_actions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "admin_actions_service_all" ON "audit"."admin_actions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "audit"."attestations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "attestations_authenticated_read" ON "audit"."attestations" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "attestations_service_insert" ON "audit"."attestations" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "audit_events_actor_select" ON "audit"."events" FOR SELECT TO "authenticated" USING (("actor_id" = "auth"."uid"()));



CREATE POLICY "audit_events_deny_write" ON "audit"."events" AS RESTRICTIVE FOR INSERT TO "authenticated" WITH CHECK (false);



CREATE POLICY "audit_events_service_all" ON "audit"."events" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "audit_security_events_deny_authenticated" ON "audit"."security_events" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



CREATE POLICY "audit_security_events_service_all" ON "audit"."security_events" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "audit"."byok_key_usage" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "byok_key_usage_deny_write" ON "audit"."byok_key_usage" AS RESTRICTIVE FOR INSERT TO "authenticated" WITH CHECK (false);



CREATE POLICY "byok_key_usage_owner_select" ON "audit"."byok_key_usage" FOR SELECT TO "authenticated" USING (("key_id" IN ( SELECT "bk"."id"
   FROM (("execution"."byok_keys" "bk"
     JOIN "agents"."ai_lensers" "al" ON (("al"."id" = "bk"."agent_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "al"."profile_id")))
  WHERE ("p"."id" = "auth"."uid"()))));



CREATE POLICY "byok_key_usage_service_all" ON "audit"."byok_key_usage" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "audit"."disputes" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "disputes_insert_authenticated" ON "audit"."disputes" FOR INSERT TO "authenticated" WITH CHECK (("disputer_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "disputes_read_own" ON "audit"."disputes" FOR SELECT USING ((("disputer_lenser_id" = "lensers"."get_auth_lenser_id"()) OR ("resolved_by" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "disputes_update_service" ON "audit"."disputes" FOR UPDATE TO "service_role" USING (true);



ALTER TABLE "audit"."events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "events_select_own" ON "audit"."events" FOR SELECT TO "authenticated" USING (("actor_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "events_service_insert" ON "audit"."events" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "events_service_select" ON "audit"."events" FOR SELECT TO "service_role" USING (true);



ALTER TABLE "audit"."hash_chains" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "hash_chains_authenticated_read" ON "audit"."hash_chains" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "hash_chains_service_insert" ON "audit"."hash_chains" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "lifecycle_audit_service_all" ON "audit"."account_lifecycle" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "mod_decisions_service_all" ON "audit"."moderation_decisions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "audit"."moderation_decisions" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "audit"."security_events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "security_events_select_own" ON "audit"."security_events" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "security_events_service_all" ON "audit"."security_events" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "audit"."vote_anomalies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "vote_anomalies_admin_select" ON "audit"."vote_anomalies" FOR SELECT TO "authenticated" USING ("public"."fn_is_super_admin"());



CREATE POLICY "vote_anomalies_deny_write" ON "audit"."vote_anomalies" TO "authenticated" USING (false) WITH CHECK (false);



CREATE POLICY "vote_anomalies_own_select" ON "audit"."vote_anomalies" FOR SELECT TO "authenticated" USING (("voter_lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "audit"."webhook_outbox" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "webhook_outbox_service" ON "audit"."webhook_outbox" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "webhook_outbox_service_role_all" ON "audit"."webhook_outbox" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "automation_events_owner_select" ON "automation"."events" FOR SELECT TO "authenticated" USING (("source_lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "automation"."cron_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "cron_runs_service_only" ON "automation"."cron_runs" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "automation"."event_dispatches" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "event_dispatches_owner_select" ON "automation"."event_dispatches" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "automation"."trigger_rules" "tr"
  WHERE (("tr"."id" = "event_dispatches"."rule_id") AND ("tr"."lenser_id" = "auth"."uid"())))));



ALTER TABLE "automation"."events" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "automation"."trigger_rules" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "trigger_rules_owner_delete" ON "automation"."trigger_rules" FOR DELETE TO "authenticated" USING (("lenser_id" = "auth"."uid"()));



CREATE POLICY "trigger_rules_owner_insert" ON "automation"."trigger_rules" FOR INSERT TO "authenticated" WITH CHECK (("lenser_id" = "auth"."uid"()));



CREATE POLICY "trigger_rules_owner_select" ON "automation"."trigger_rules" FOR SELECT TO "authenticated" USING (("lenser_id" = "auth"."uid"()));



CREATE POLICY "trigger_rules_owner_update" ON "automation"."trigger_rules" FOR UPDATE TO "authenticated" USING (("lenser_id" = "auth"."uid"())) WITH CHECK (("lenser_id" = "auth"."uid"()));



CREATE POLICY "Authenticated can insert contenders on open battles" ON "battles"."contenders" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "contenders"."battle_id") AND (("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()) OR ("b"."status" = 'open'::"battles"."battle_status_enum"))))));



CREATE POLICY "Authenticated users can create battles" ON "battles"."battles" FOR INSERT TO "authenticated" WITH CHECK (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Authenticated users can create rubrics" ON "battles"."rubrics" FOR INSERT TO "authenticated" WITH CHECK (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Authenticated users can create templates" ON "battles"."templates" FOR INSERT TO "authenticated" WITH CHECK (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Authors can delete own templates" ON "battles"."templates" FOR DELETE TO "authenticated" USING (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Authors can insert criteria on own rubrics" ON "battles"."rubric_criteria" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."rubrics" "r"
  WHERE (("r"."id" = "rubric_criteria"."rubric_id") AND ("r"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "Authors can update criteria on own rubrics" ON "battles"."rubric_criteria" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."rubrics" "r"
  WHERE (("r"."id" = "rubric_criteria"."rubric_id") AND ("r"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "Authors can update own draft or open battles" ON "battles"."battles" FOR UPDATE TO "authenticated" USING ((("creator_lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("status" = ANY (ARRAY['draft'::"battles"."battle_status_enum", 'open'::"battles"."battle_status_enum"])))) WITH CHECK (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Authors can update own rubrics" ON "battles"."rubrics" FOR UPDATE TO "authenticated" USING (("creator_lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Authors can update own templates" ON "battles"."templates" FOR UPDATE TO "authenticated" USING (("creator_lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("creator_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Battle creators can insert invitations" ON "battles"."invitations" FOR INSERT TO "authenticated" WITH CHECK ((("invited_by" = "lensers"."get_auth_lenser_id"()) AND (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "invitations"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("b"."status" = ANY (ARRAY['draft'::"battles"."battle_status_enum", 'open'::"battles"."battle_status_enum"])))))));



CREATE POLICY "Contenders can insert submissions on open battles" ON "battles"."submissions" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("battles"."contenders" "c"
     JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
  WHERE (("c"."id" = "submissions"."contender_id") AND ("c"."contender_type" = 'human'::"battles"."contender_type_enum") AND ("c"."contender_ref_id" = "lensers"."get_auth_lenser_id"()) AND ("b"."status" = 'open'::"battles"."battle_status_enum")))));



CREATE POLICY "Contenders can update own submissions on open battles" ON "battles"."submissions" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("battles"."contenders" "c"
     JOIN "battles"."battles" "b" ON (("b"."id" = "c"."battle_id")))
  WHERE (("c"."id" = "submissions"."contender_id") AND ("c"."contender_type" = 'human'::"battles"."contender_type_enum") AND ("c"."contender_ref_id" = "lensers"."get_auth_lenser_id"()) AND ("b"."status" = 'open'::"battles"."battle_status_enum")))));



CREATE POLICY "Invitees can update invitation status" ON "battles"."invitations" FOR UPDATE TO "authenticated" USING (("invited_lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("invited_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "Scorecards on closed+ battles are viewable" ON "battles"."scorecards" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "scorecards"."battle_id") AND ("b"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))));



CREATE POLICY "Voters can delete own vote during voting" ON "battles"."votes" FOR DELETE TO "authenticated" USING ((("voter_lenser_id" = "lensers"."get_auth_lenser_id"()) AND (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "votes"."battle_id") AND ("b"."status" = 'voting'::"battles"."battle_status_enum"))))));



ALTER TABLE "battles"."ai_handicap_policies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "ai_handicap_select_authenticated" ON "battles"."ai_handicap_policies" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "ai_handicap_write_service_role" ON "battles"."ai_handicap_policies" TO "service_role" USING (true);



ALTER TABLE "battles"."ai_judge_verdicts" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "ai_judge_verdicts_public_read" ON "battles"."ai_judge_verdicts" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "ai_judge_verdicts"."battle_id") AND ("b"."status" = ANY (ARRAY['published'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum"]))))));



CREATE POLICY "ai_judge_verdicts_service_all" ON "battles"."ai_judge_verdicts" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "battle_dlq_service_all" ON "battles"."battle_execution_dead_letters" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."battle_event_subscriptions" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."battle_execution_dead_letters" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."battle_execution_jobs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "battle_subscriptions_owner" ON "battles"."battle_event_subscriptions" TO "authenticated" USING (("owner_id" = "auth"."uid"())) WITH CHECK (("owner_id" = "auth"."uid"()));



CREATE POLICY "battle_subscriptions_service" ON "battles"."battle_event_subscriptions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."battles" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "battles_select" ON "battles"."battles" FOR SELECT USING (((("status" = ANY (ARRAY['open'::"battles"."battle_status_enum", 'executing'::"battles"."battle_status_enum", 'voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"])) AND ("deleted_at" IS NULL)) OR ("creator_lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "cla_insert_battle_creator" ON "battles"."contender_lens_assignments" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("battles"."battles" "b"
     JOIN "battles"."contenders" "c" ON (("c"."battle_id" = "b"."id")))
  WHERE (("c"."id" = "contender_lens_assignments"."contender_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "cla_insert_own" ON "battles"."contender_lens_assignments" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."contenders" "c"
  WHERE (("c"."id" = "contender_lens_assignments"."contender_id") AND ("c"."contender_ref_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "cla_public_read" ON "battles"."contender_lens_assignments" FOR SELECT USING (true);



CREATE POLICY "cla_service_write" ON "battles"."contender_lens_assignments" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."comments" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "comments_owner_delete" ON "battles"."comments" FOR DELETE USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "comments_owner_insert" ON "battles"."comments" FOR INSERT WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "comments_public_read" ON "battles"."comments" FOR SELECT USING (true);



ALTER TABLE "battles"."contender_entity_map" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."contender_lens_assignments" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."contender_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "contender_runs_select_authenticated" ON "battles"."contender_runs" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "contender_runs"."battle_id") AND ("b"."deleted_at" IS NULL) AND ("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"]))))));



CREATE POLICY "contender_runs_service_all" ON "battles"."contender_runs" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."contenders" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "contenders_select" ON "battles"."contenders" FOR SELECT USING (((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "contenders"."battle_id") AND ("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))) OR (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "contenders"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (("contender_type" = 'human'::"battles"."contender_type_enum") AND ("contender_ref_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid")))))));



CREATE POLICY "contribs_select" ON "battles"."sponsorship_contributions" FOR SELECT TO "authenticated" USING ((("contributor_lenser_id" = "lensers"."get_auth_lenser_id"()) OR (EXISTS ( SELECT 1
   FROM ("battles"."sponsorship_pools" "p"
     JOIN "battles"."battles" "b" ON (("b"."id" = "p"."battle_id")))
  WHERE (("p"."id" = "sponsorship_contributions"."pool_id") AND ("b"."status" = ANY (ARRAY['open'::"battles"."battle_status_enum", 'voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL))))));



CREATE POLICY "contribs_service_all" ON "battles"."sponsorship_contributions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."cost_ledgers" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "cost_ledgers_select_creator" ON "battles"."cost_ledgers" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "cost_ledgers"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "cost_ledgers_service_all" ON "battles"."cost_ledgers" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "entity_map_public_read" ON "battles"."contender_entity_map" FOR SELECT USING (true);



CREATE POLICY "entity_map_service_write" ON "battles"."contender_entity_map" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "events_select" ON "battles"."events" FOR SELECT USING (((( SELECT "auth"."role"() AS "role") = 'service_role'::"text") OR (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "events"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "events_service_delete" ON "battles"."events" FOR DELETE USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "events_service_update" ON "battles"."events" FOR UPDATE USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text")) WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "events_service_write" ON "battles"."events" FOR INSERT WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



ALTER TABLE "battles"."execution_configs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "execution_configs_insert" ON "battles"."execution_configs" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "execution_configs"."battle_id") AND ("b"."creator_lenser_id" = "auth"."uid"())))));



CREATE POLICY "execution_configs_select" ON "battles"."execution_configs" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "execution_configs"."battle_id") AND ("b"."status" <> 'draft'::"battles"."battle_status_enum")))));



CREATE POLICY "execution_configs_service" ON "battles"."execution_configs" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "execution_configs_update" ON "battles"."execution_configs" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "execution_configs"."battle_id") AND ("b"."creator_lenser_id" = "auth"."uid"())))));



ALTER TABLE "battles"."funding_policies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "funding_policies_insert_creator" ON "battles"."funding_policies" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "funding_policies"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("b"."status" = 'draft'::"battles"."battle_status_enum")))));



CREATE POLICY "funding_policies_select_creator" ON "battles"."funding_policies" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "funding_policies"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "funding_policies_service_all" ON "battles"."funding_policies" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "funding_policies_update_creator" ON "battles"."funding_policies" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "funding_policies"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("b"."status" = 'draft'::"battles"."battle_status_enum"))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "funding_policies"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("b"."status" = 'draft'::"battles"."battle_status_enum")))));



ALTER TABLE "battles"."generated_challenges" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "generated_challenges_select_creator" ON "battles"."generated_challenges" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "generated_challenges"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "generated_challenges_select_public" ON "battles"."generated_challenges" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "generated_challenges"."battle_id") AND ("b"."status" = ANY (ARRAY['open'::"battles"."battle_status_enum", 'executing'::"battles"."battle_status_enum", 'voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))));



CREATE POLICY "generated_challenges_service_insert" ON "battles"."generated_challenges" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "generated_challenges_service_update" ON "battles"."generated_challenges" FOR UPDATE TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."global_messages" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "global_messages_owner_delete" ON "battles"."global_messages" FOR DELETE USING (("sender_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "global_messages_owner_insert" ON "battles"."global_messages" FOR INSERT WITH CHECK (("sender_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "global_messages_public_read" ON "battles"."global_messages" FOR SELECT USING (true);



ALTER TABLE "battles"."invitations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "invitations_select" ON "battles"."invitations" FOR SELECT TO "authenticated" USING ((("invited_lenser_id" = "lensers"."get_auth_lenser_id"()) OR ("invited_by" = "lensers"."get_auth_lenser_id"())));



ALTER TABLE "battles"."invite_links" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "invite_links_insert" ON "battles"."invite_links" FOR INSERT TO "authenticated" WITH CHECK ((("created_by" = "lensers"."get_auth_lenser_id"()) AND (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "invite_links"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "invite_links_select" ON "battles"."invite_links" FOR SELECT TO "authenticated" USING (("created_by" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "invite_links_select_anon" ON "battles"."invite_links" FOR SELECT TO "anon" USING (("type" = ANY (ARRAY['public'::"battles"."invite_link_type", 'link'::"battles"."invite_link_type", 'qr'::"battles"."invite_link_type"])));



ALTER TABLE "battles"."media_quality_results" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "media_quality_results_select" ON "battles"."media_quality_results" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM ("battles"."submissions" "s"
     JOIN "battles"."battles" "b" ON (("b"."id" = "s"."battle_id")))
  WHERE (("s"."id" = "media_quality_results"."submission_id") AND (("b"."creator_lenser_id" = "auth"."uid"()) OR (EXISTS ( SELECT 1
           FROM "battles"."contenders" "c"
          WHERE (("c"."id" = "s"."contender_id") AND ("c"."contender_ref_id" = "auth"."uid"())))))))));



ALTER TABLE "battles"."media_quality_rules" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "media_quality_rules_owner_write" ON "battles"."media_quality_rules" USING ((EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "media_quality_rules"."template_id") AND ("t"."creator_lenser_id" = "auth"."uid"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "media_quality_rules"."template_id") AND ("t"."creator_lenser_id" = "auth"."uid"())))));



CREATE POLICY "media_quality_rules_select" ON "battles"."media_quality_rules" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "media_quality_rules"."template_id") AND (("t"."is_public" = true) OR ("t"."creator_lenser_id" = "auth"."uid"()))))));



ALTER TABLE "battles"."model_test_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "model_test_runs_select" ON "battles"."model_test_runs" FOR SELECT USING (((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "model_test_runs"."battle_id") AND ("b"."creator_lenser_id" = "auth"."uid"())))) OR (EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "model_test_runs"."template_id") AND ("t"."creator_lenser_id" = "auth"."uid"()))))));



CREATE POLICY "payouts_select" ON "battles"."sponsorship_payouts" FOR SELECT TO "authenticated" USING ((("recipient_lenser_id" = "lensers"."get_auth_lenser_id"()) OR (EXISTS ( SELECT 1
   FROM ("battles"."sponsorship_pools" "p"
     JOIN "battles"."battles" "b" ON (("b"."id" = "p"."battle_id")))
  WHERE (("p"."id" = "sponsorship_payouts"."pool_id") AND ("b"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL))))));



CREATE POLICY "payouts_service_all" ON "battles"."sponsorship_payouts" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "pools_select" ON "battles"."sponsorship_pools" FOR SELECT TO "authenticated" USING (((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "sponsorship_pools"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "sponsorship_pools"."battle_id") AND ("b"."status" = ANY (ARRAY['open'::"battles"."battle_status_enum", 'voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL))))));



CREATE POLICY "pools_service_all" ON "battles"."sponsorship_pools" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."rubric_criteria" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "rubric_criteria_select" ON "battles"."rubric_criteria" FOR SELECT USING (((EXISTS ( SELECT 1
   FROM "battles"."rubrics" "r"
  WHERE (("r"."id" = "rubric_criteria"."rubric_id") AND ("r"."is_public" = true) AND ("r"."deleted_at" IS NULL)))) OR (EXISTS ( SELECT 1
   FROM "battles"."rubrics" "r"
  WHERE (("r"."id" = "rubric_criteria"."rubric_id") AND ("r"."creator_lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "battles"."rubrics" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "rubrics_select" ON "battles"."rubrics" FOR SELECT USING (((("is_public" = true) AND ("deleted_at" IS NULL)) OR ("creator_lenser_id" = "lensers"."get_auth_lenser_id"())));



ALTER TABLE "battles"."rule_snapshots" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "rule_snapshots_public_read" ON "battles"."rule_snapshots" FOR SELECT USING (true);



CREATE POLICY "rule_snapshots_service_insert" ON "battles"."rule_snapshots" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "schedule_owner_select" ON "battles"."schedules" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "schedules"."battle_id") AND ("b"."creator_lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"())))))));



ALTER TABLE "battles"."schedules" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."scorecards" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."scoring_plugin_signals" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "scoring_plugin_signals_battle_creator_select" ON "battles"."scoring_plugin_signals" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "scoring_plugin_signals"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))));



ALTER TABLE "battles"."series" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."series_rounds" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "series_rounds_select" ON "battles"."series_rounds" FOR SELECT USING ((("battles"."fn_series_round_owner_id"("series_id") = "auth"."uid"()) OR (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "series_rounds"."battle_id") AND (("b"."status")::"text" = 'published'::"text"))))));



CREATE POLICY "series_select" ON "battles"."series" FOR SELECT USING ((("creator_lenser_id" = "auth"."uid"()) OR "battles"."fn_series_has_published_round"("id")));



CREATE POLICY "service_role_all_battle_execution_jobs" ON "battles"."battle_execution_jobs" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."sponsorship_contributions" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."sponsorship_payouts" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."sponsorship_pools" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."stream_recordings" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "stream_recordings_insert" ON "battles"."stream_recordings" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "stream_recordings"."battle_id") AND ("b"."creator_lenser_id" = "auth"."uid"())))));



CREATE POLICY "stream_recordings_select" ON "battles"."stream_recordings" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "stream_recordings"."battle_id") AND ("b"."status" <> 'draft'::"battles"."battle_status_enum")))));



CREATE POLICY "stream_recordings_service" ON "battles"."stream_recordings" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."submissions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "submissions_select" ON "battles"."submissions" FOR SELECT USING (((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "submissions"."battle_id") AND ("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))) OR (EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "submissions"."battle_id") AND ("b"."creator_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (EXISTS ( SELECT 1
   FROM "battles"."contenders" "c"
  WHERE (("c"."id" = "submissions"."contender_id") AND ("c"."contender_type" = 'human'::"battles"."contender_type_enum") AND ("c"."contender_ref_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "battles"."template_prompt_variables" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "battles"."templates" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "templates_select" ON "battles"."templates" FOR SELECT USING (((("is_public" = true) AND ("deleted_at" IS NULL)) OR ("creator_lenser_id" = "lensers"."get_auth_lenser_id"())));



ALTER TABLE "battles"."tool_registry" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tool_registry_insert_service" ON "battles"."tool_registry" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "tool_registry_select_anon" ON "battles"."tool_registry" FOR SELECT TO "anon" USING (true);



CREATE POLICY "tool_registry_select_authenticated" ON "battles"."tool_registry" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "tool_registry_update_service" ON "battles"."tool_registry" FOR UPDATE TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."tournament_contenders" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tournament_contenders_public_read" ON "battles"."tournament_contenders" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "tournament_contenders_service_all" ON "battles"."tournament_contenders" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."tournament_matches" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tournament_matches_public_read" ON "battles"."tournament_matches" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "tournament_matches_service_all" ON "battles"."tournament_matches" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."tournament_rounds" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tournament_rounds_public_read" ON "battles"."tournament_rounds" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "tournament_rounds_service_all" ON "battles"."tournament_rounds" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."tournaments" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tournaments_creator_update" ON "battles"."tournaments" FOR UPDATE TO "authenticated" USING (("creator_lenser_id" = "auth"."uid"()));



CREATE POLICY "tournaments_public_read" ON "battles"."tournaments" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "tournaments_service_all" ON "battles"."tournaments" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "tpl_prompt_vars_owner_write" ON "battles"."template_prompt_variables" USING ((EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "template_prompt_variables"."template_id") AND ("t"."creator_lenser_id" = "auth"."uid"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "template_prompt_variables"."template_id") AND ("t"."creator_lenser_id" = "auth"."uid"())))));



CREATE POLICY "tpl_prompt_vars_select" ON "battles"."template_prompt_variables" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "battles"."templates" "t"
  WHERE (("t"."id" = "template_prompt_variables"."template_id") AND (("t"."is_public" = true) OR ("t"."creator_lenser_id" = "auth"."uid"()))))));



ALTER TABLE "battles"."vote_aggregates" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "vote_aggregates_select_public" ON "battles"."vote_aggregates" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "vote_aggregates"."battle_id") AND ("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))));



CREATE POLICY "vote_aggregates_service_all" ON "battles"."vote_aggregates" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."vote_choices" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "vote_choices_select_public" ON "battles"."vote_choices" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("battles"."votes" "v"
     JOIN "battles"."battles" "b" ON (("b"."id" = "v"."battle_id")))
  WHERE (("v"."id" = "vote_choices"."vote_id") AND ("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))));



CREATE POLICY "vote_choices_service_all" ON "battles"."vote_choices" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "battles"."votes" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "votes_select" ON "battles"."votes" FOR SELECT USING (((EXISTS ( SELECT 1
   FROM "battles"."battles" "b"
  WHERE (("b"."id" = "votes"."battle_id") AND ("b"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL)))) OR ("voter_lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "billing_margin_policies_service_all" ON "billing"."execution_margin_policies" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "billing"."budget_policies" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."checkout_sessions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "checkout_sessions_service_all" ON "billing"."checkout_sessions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "billing"."cost_reservations" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."execution_margin_policies" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."ledger_entries" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."orders" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "orders_service_all" ON "billing"."orders" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "billing"."pricing_snapshots" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."products" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "products_service_all" ON "billing"."products" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "billing"."provider_circuit_state" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "rls_budget_policies_service_all" ON "billing"."budget_policies" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "rls_cost_reservations_owner_select" ON "billing"."cost_reservations" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("agents"."ai_lensers" "al"
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "al"."profile_id")))
  WHERE (("al"."id" = "cost_reservations"."ai_lenser_id") AND ("p"."user_id" = "auth"."uid"())))));



CREATE POLICY "rls_cost_reservations_service_all" ON "billing"."cost_reservations" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "rls_ledger_entries_owner_select" ON "billing"."ledger_entries" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("agents"."ai_lensers" "al"
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "al"."profile_id")))
  WHERE (("al"."id" = "ledger_entries"."ai_lenser_id") AND ("p"."user_id" = "auth"."uid"())))));



CREATE POLICY "rls_ledger_entries_service_all" ON "billing"."ledger_entries" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "rls_pricing_snapshots_service_all" ON "billing"."pricing_snapshots" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "rls_provider_circuit_service_all" ON "billing"."provider_circuit_state" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "rls_runtime_settings_auth_read" ON "billing"."runtime_settings" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "rls_runtime_settings_service_all" ON "billing"."runtime_settings" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "rls_spend_anomalies_service_all" ON "billing"."spend_anomalies" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "billing"."runtime_settings" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."spend_anomalies" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "billing"."variants" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "variants_service_all" ON "billing"."variants" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "billing"."webhook_events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "webhook_events_service_all" ON "billing"."webhook_events" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "connectors"."connector_tokens" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "connector_tokens_deny_all" ON "connectors"."connector_tokens" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



ALTER TABLE "connectors"."connectors" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "connectors_deny_all" ON "connectors"."connectors" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



CREATE POLICY "Public can read tag maps" ON "content"."tag_map" FOR SELECT USING (true);



CREATE POLICY "Public can read tag translations" ON "content"."tag_translations" FOR SELECT USING (true);



ALTER TABLE "content"."entity_translations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "entity_translations_delete_author" ON "content"."entity_translations" FOR DELETE TO "authenticated" USING (((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "pt"
  WHERE (("pt"."id" = "entity_translations"."entity_id") AND ("pt"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("entity_translations"."entity_type" = 'lens'::"content"."entity_type_enum")))) OR (EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "entity_translations"."entity_id") AND ("t"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("entity_translations"."entity_type" = 'thread'::"content"."entity_type_enum"))))));



CREATE POLICY "entity_translations_insert_author" ON "content"."entity_translations" FOR INSERT TO "authenticated" WITH CHECK (((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "pt"
  WHERE (("pt"."id" = "entity_translations"."entity_id") AND ("pt"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("entity_translations"."entity_type" = 'lens'::"content"."entity_type_enum")))) OR (EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "entity_translations"."entity_id") AND ("t"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("entity_translations"."entity_type" = 'thread'::"content"."entity_type_enum"))))));



CREATE POLICY "entity_translations_select_public" ON "content"."entity_translations" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "entity_translations_service_all" ON "content"."entity_translations" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "entity_translations_update_author" ON "content"."entity_translations" FOR UPDATE TO "authenticated" USING (((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "pt"
  WHERE (("pt"."id" = "entity_translations"."entity_id") AND ("pt"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("entity_translations"."entity_type" = 'lens'::"content"."entity_type_enum")))) OR (EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "entity_translations"."entity_id") AND ("t"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("entity_translations"."entity_type" = 'thread'::"content"."entity_type_enum"))))));



CREATE POLICY "lensers_can_insert_tags" ON "content"."tags" FOR INSERT WITH CHECK ((( SELECT "auth"."uid"() AS "uid") IN ( SELECT "profiles"."user_id"
   FROM "lensers"."profiles")));



CREATE POLICY "no_delete_tags" ON "content"."tags" FOR DELETE USING (false);



CREATE POLICY "no_update_tags" ON "content"."tags" FOR UPDATE USING (false) WITH CHECK (false);



CREATE POLICY "public_can_read_public_tags" ON "content"."tags" FOR SELECT USING (("visibility" = 'public'::"content"."tag_visibility_enum"));



ALTER TABLE "content"."reactions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "reactions_delete_own" ON "content"."reactions" FOR DELETE TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "reactions_insert_own" ON "content"."reactions" FOR INSERT TO "authenticated" WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "reactions_select_public" ON "content"."reactions" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "reactions_service_all" ON "content"."reactions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "content"."reports" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "reports_delete_own" ON "content"."reports" FOR DELETE USING (("reporter_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "reports_insert_self" ON "content"."reports" FOR INSERT WITH CHECK (("reporter_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "reports_select_own" ON "content"."reports" FOR SELECT USING (("reporter_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "service_role_full_access_tags" ON "content"."tags" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "content"."tag_map" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tag_map_author_delete" ON "content"."tag_map" FOR DELETE TO "authenticated" USING (((("entity_type" = 'lens'::"content"."entity_type_enum") AND (EXISTS ( SELECT 1
   FROM "lenses"."lenses" "p"
  WHERE (("p"."id" = "tag_map"."entity_id") AND ("p"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) OR (("entity_type" = 'thread'::"content"."entity_type_enum") AND (EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "tag_map"."entity_id") AND ("t"."lenser_id" = "lensers"."get_auth_lenser_id"())))))));



CREATE POLICY "tag_map_author_manage" ON "content"."tag_map" FOR INSERT TO "authenticated" WITH CHECK (((("entity_type" = 'lens'::"content"."entity_type_enum") AND (EXISTS ( SELECT 1
   FROM "lenses"."lenses" "p"
  WHERE (("p"."id" = "tag_map"."entity_id") AND ("p"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) OR (("entity_type" = 'thread'::"content"."entity_type_enum") AND (EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "tag_map"."entity_id") AND ("t"."lenser_id" = "lensers"."get_auth_lenser_id"())))))));



ALTER TABLE "content"."tag_suggestions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tag_suggestions_service_all" ON "content"."tag_suggestions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "content"."tag_translations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tag_translations_service_delete" ON "content"."tag_translations" FOR DELETE USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "tag_translations_service_update" ON "content"."tag_translations" FOR UPDATE USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text")) WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "tag_translations_service_write" ON "content"."tag_translations" FOR INSERT WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



ALTER TABLE "content"."tags" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "content"."thread_replies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "thread_replies_owner_delete" ON "content"."thread_replies" FOR DELETE USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "thread_replies_owner_insert" ON "content"."thread_replies" FOR INSERT WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "thread_replies_owner_update" ON "content"."thread_replies" FOR UPDATE USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"()))) WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "thread_replies_select" ON "content"."thread_replies" FOR SELECT USING ((((EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "thread_replies"."thread_id") AND ("t"."visibility" = ANY (ARRAY['public'::"content"."visibility_enum", 'community'::"content"."visibility_enum"])) AND ("t"."status" = 'published'::"content"."content_status")))) AND ("status" = 'published'::"content"."thread_reply_status") AND ("deleted_at" IS NULL)) OR ("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())) OR (EXISTS ( SELECT 1
   FROM "content"."threads" "t"
  WHERE (("t"."id" = "thread_replies"."thread_id") AND ("t"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "thread_replies_service_role_all" ON "content"."thread_replies" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "content"."threads" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "threads_owner_delete" ON "content"."threads" FOR DELETE USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "threads_owner_insert" ON "content"."threads" FOR INSERT WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "threads_owner_update" ON "content"."threads" FOR UPDATE USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"()))) WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "threads_select" ON "content"."threads" FOR SELECT USING (((("visibility" = ANY (ARRAY['public'::"content"."visibility_enum", 'community'::"content"."visibility_enum"])) AND ("status" = 'published'::"content"."content_status") AND (EXISTS ( SELECT 1
   FROM "lensers"."profiles" "p"
  WHERE (("p"."id" = "threads"."lenser_id") AND ("p"."status" = 'active'::"lensers"."lenser_status"))))) OR ("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"()))));



CREATE POLICY "threads_service_role_all" ON "content"."threads" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."cities" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "cities_select_public" ON "core"."cities" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "cities_service_all" ON "core"."cities" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."countries" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "countries_select_public" ON "core"."countries" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "countries_service_all" ON "core"."countries" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."currencies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "currencies_select_public" ON "core"."currencies" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "currencies_service_all" ON "core"."currencies" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."languages" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "languages_select_active" ON "core"."languages" FOR SELECT TO "authenticated", "anon" USING (("is_active" = true));



CREATE POLICY "languages_service_all" ON "core"."languages" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."regions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "regions_select_public" ON "core"."regions" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "regions_service_all" ON "core"."regions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."states" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "states_select_public" ON "core"."states" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "states_service_all" ON "core"."states" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "core"."subregions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "subregions_select_public" ON "core"."subregions" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "subregions_service_all" ON "core"."subregions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "devices"."device_challenges" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "device_challenges_owner_select" ON "devices"."device_challenges" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



CREATE POLICY "devices_owner_delete" ON "devices"."registered_devices" FOR DELETE USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



CREATE POLICY "devices_owner_select" ON "devices"."registered_devices" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



ALTER TABLE "devices"."nonce_cache" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "nonce_cache_deny_all" ON "devices"."nonce_cache" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



ALTER TABLE "devices"."peer_leases" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "peer_leases_owner_select" ON "devices"."peer_leases" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



ALTER TABLE "devices"."registered_devices" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "devices"."sync_outbox" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "sync_outbox_owner_select" ON "devices"."sync_outbox" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



ALTER TABLE "devices"."sync_watermarks" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "sync_watermarks_owner_select" ON "devices"."sync_watermarks" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



ALTER TABLE "execution"."artifact_medias" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "artifact_medias_service_all" ON "execution"."artifact_medias" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "execution"."artifacts" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "artifacts_select" ON "execution"."artifacts" FOR SELECT USING (((EXISTS ( SELECT 1
   FROM ("execution"."runs" "ru"
     JOIN "execution"."requests" "rq" ON (("rq"."id" = "ru"."request_id")))
  WHERE (("ru"."id" = "artifacts"."run_id") AND ("rq"."requester_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (("visibility" = 'public'::"text") AND ((( SELECT "auth"."uid"() AS "uid") IS NOT NULL) OR (EXISTS ( SELECT 1
   FROM "execution"."runs" "r"
  WHERE (("r"."id" = "artifacts"."run_id") AND ("r"."status" = 'succeeded'::"text"))))))));



CREATE POLICY "artifacts_service_all" ON "execution"."artifacts" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "artifacts_service_role_all" ON "execution"."artifacts" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "att_verifications_owner_select" ON "execution"."attestation_verifications" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM ((("execution"."attestations" "a"
     JOIN "execution"."runs" "r" ON (("r"."id" = "a"."run_id")))
     JOIN "execution"."requests" "req" ON (("req"."id" = "r"."request_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "req"."requester_lenser_id")))
  WHERE (("a"."id" = "attestation_verifications"."attestation_id") AND ("p"."user_id" = "auth"."uid"())))));



ALTER TABLE "execution"."attestation_verifications" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "execution"."attestations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "attestations_owner_select" ON "execution"."attestations" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM (("execution"."runs" "r"
     JOIN "execution"."requests" "req" ON (("req"."id" = "r"."request_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "req"."requester_lenser_id")))
  WHERE (("r"."id" = "attestations"."run_id") AND ("p"."user_id" = "auth"."uid"())))));



ALTER TABLE "execution"."byok_keys" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "byok_keys_deny_all" ON "execution"."byok_keys" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



CREATE POLICY "execution_links_owner_select" ON "execution"."links" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM (("execution"."runs" "r"
     JOIN "execution"."requests" "req" ON (("req"."id" = "r"."request_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "req"."requester_lenser_id")))
  WHERE (("r"."id" = "links"."run_id") AND ("p"."user_id" = "auth"."uid"())))));



ALTER TABLE "execution"."execution_tags" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "execution_tags_service_all" ON "execution"."execution_tags" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "lenser reads own parameter usage logs" ON "execution"."parameter_usage_logs" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "execution"."requests" "r"
  WHERE (("r"."id" = "parameter_usage_logs"."request_id") AND ("r"."requester_lenser_id" = "lensers"."get_auth_lenser_id"())))));



ALTER TABLE "execution"."links" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "execution"."origin_types" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "origin_types_read_active" ON "execution"."origin_types" FOR SELECT TO "authenticated", "anon" USING (("is_active" = true));



CREATE POLICY "origin_types_service_all" ON "execution"."origin_types" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "execution"."parameter_usage_logs" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "execution"."request_attachments" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "request_attachments_own" ON "execution"."request_attachments" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "execution"."requests" "req"
  WHERE (("req"."id" = "request_attachments"."request_id") AND ("req"."requester_lenser_id" = "lensers"."get_auth_lenser_id"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "execution"."requests" "req"
  WHERE (("req"."id" = "request_attachments"."request_id") AND ("req"."requester_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "request_attachments_service_role" ON "execution"."request_attachments" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "execution"."requests" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "requests_insert_own" ON "execution"."requests" FOR INSERT TO "authenticated" WITH CHECK (("requester_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "requests_select_own" ON "execution"."requests" FOR SELECT TO "authenticated" USING (("requester_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "requests_service_all" ON "execution"."requests" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "execution"."runner_device_bindings" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "runner_device_bindings_deny_all" ON "execution"."runner_device_bindings" AS RESTRICTIVE TO "authenticated", "anon" USING (false);



ALTER TABLE "execution"."runners" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "runners_owner_select" ON "execution"."runners" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"())
 LIMIT 1)));



ALTER TABLE "execution"."runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "runs_select_own" ON "execution"."runs" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "execution"."requests" "r"
  WHERE (("r"."id" = "runs"."request_id") AND ("r"."requester_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "runs_service_all" ON "execution"."runs" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "execution"."steps" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "steps_select_own" ON "execution"."steps" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("execution"."runs" "ru"
     JOIN "execution"."requests" "rq" ON (("rq"."id" = "ru"."request_id")))
  WHERE (("ru"."id" = "steps"."run_id") AND ("rq"."requester_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "steps_service_all" ON "execution"."steps" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "execution"."stream_sessions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "stream_sessions_service_all" ON "execution"."stream_sessions" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "trust_evals_owner_select" ON "execution"."trust_evaluations" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM ((("battles"."submissions" "s"
     JOIN "execution"."runs" "r" ON (("r"."id" = "s"."execution_run_id")))
     JOIN "execution"."requests" "req" ON (("req"."id" = "r"."request_id")))
     JOIN "lensers"."profiles" "p" ON (("p"."id" = "req"."requester_lenser_id")))
  WHERE (("s"."id" = "trust_evaluations"."submission_id") AND ("p"."user_id" = "auth"."uid"())))));



ALTER TABLE "execution"."trust_evaluations" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "identity_gov"."reserved_namespaces" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "reserved_namespaces_select" ON "identity_gov"."reserved_namespaces" FOR SELECT TO "authenticated", "anon" USING (true);



ALTER TABLE "integrations"."tool_allowlist" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tool_allowlist_service_all" ON "integrations"."tool_allowlist" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "integrations"."tools" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tools_select_active" ON "integrations"."tools" FOR SELECT TO "authenticated", "anon" USING (("is_active" = true));



CREATE POLICY "tools_service_all" ON "integrations"."tools" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "Public can read badges" ON "lensers"."badges" FOR SELECT USING (true);



ALTER TABLE "lensers"."badges" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "badges_no_write" ON "lensers"."badges" AS RESTRICTIVE TO "authenticated", "anon" USING (true) WITH CHECK (false);



CREATE POLICY "badges_service_write" ON "lensers"."badges" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lensers"."group_members" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "group_members_delete_admin" ON "lensers"."group_members" FOR DELETE TO "authenticated" USING (((EXISTS ( SELECT 1
   FROM "lensers"."groups" "g"
  WHERE (("g"."id" = "group_members"."group_id") AND ("g"."owner_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (EXISTS ( SELECT 1
   FROM "lensers"."group_members" "gm"
  WHERE (("gm"."group_id" = "group_members"."group_id") AND ("gm"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("gm"."role" = 'admin'::"lensers"."group_member_role_enum"))))));



CREATE POLICY "group_members_insert_admin" ON "lensers"."group_members" FOR INSERT TO "authenticated" WITH CHECK (((EXISTS ( SELECT 1
   FROM "lensers"."groups" "g"
  WHERE (("g"."id" = "group_members"."group_id") AND ("g"."owner_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (EXISTS ( SELECT 1
   FROM "lensers"."group_members" "gm"
  WHERE (("gm"."group_id" = "group_members"."group_id") AND ("gm"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("gm"."role" = 'admin'::"lensers"."group_member_role_enum"))))));



CREATE POLICY "group_members_select_member" ON "lensers"."group_members" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lensers"."group_members" "my"
  WHERE (("my"."group_id" = "group_members"."group_id") AND ("my"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "group_members_service_all" ON "lensers"."group_members" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "group_members_update_admin" ON "lensers"."group_members" FOR UPDATE TO "authenticated" USING (((EXISTS ( SELECT 1
   FROM "lensers"."groups" "g"
  WHERE (("g"."id" = "group_members"."group_id") AND ("g"."owner_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (EXISTS ( SELECT 1
   FROM "lensers"."group_members" "gm"
  WHERE (("gm"."group_id" = "group_members"."group_id") AND ("gm"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("gm"."role" = 'admin'::"lensers"."group_member_role_enum")))))) WITH CHECK (((EXISTS ( SELECT 1
   FROM "lensers"."groups" "g"
  WHERE (("g"."id" = "group_members"."group_id") AND ("g"."owner_lenser_id" = "lensers"."get_auth_lenser_id"())))) OR (EXISTS ( SELECT 1
   FROM "lensers"."group_members" "gm"
  WHERE (("gm"."group_id" = "group_members"."group_id") AND ("gm"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("gm"."role" = 'admin'::"lensers"."group_member_role_enum"))))));



ALTER TABLE "lensers"."groups" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "groups_insert_own" ON "lensers"."groups" FOR INSERT TO "authenticated" WITH CHECK (("owner_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "groups_select" ON "lensers"."groups" FOR SELECT TO "authenticated", "anon" USING ((("visibility" = 'public'::"lensers"."group_visibility_enum") OR (("visibility" = ANY (ARRAY['private'::"lensers"."group_visibility_enum", 'invite_only'::"lensers"."group_visibility_enum"])) AND (EXISTS ( SELECT 1
   FROM "lensers"."group_members" "gm"
  WHERE (("gm"."group_id" = "groups"."id") AND ("gm"."lenser_id" = "lensers"."get_auth_lenser_id"())))))));



CREATE POLICY "groups_service_all" ON "lensers"."groups" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "groups_update_own" ON "lensers"."groups" FOR UPDATE TO "authenticated" USING (("owner_lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("owner_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "lensers_deny_delete" ON "lensers"."profiles" FOR DELETE USING (false);



CREATE POLICY "lensers_owner_insert" ON "lensers"."profiles" FOR INSERT WITH CHECK (((( SELECT "auth"."uid"() AS "uid") IS NOT NULL) AND ("user_id" = ( SELECT "auth"."uid"() AS "uid"))));



CREATE POLICY "lensers_service_role_all" ON "lensers"."profiles" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "pref_owner_insert" ON "lensers"."preferences" FOR INSERT TO "authenticated" WITH CHECK (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "pref_owner_select" ON "lensers"."preferences" FOR SELECT TO "authenticated" USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "pref_owner_update" ON "lensers"."preferences" FOR UPDATE TO "authenticated" USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "pref_service_all" ON "lensers"."preferences" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lensers"."preferences" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lensers"."profiles" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "profiles_owner_update" ON "lensers"."profiles" FOR UPDATE TO "authenticated" USING (("user_id" = ( SELECT "auth"."uid"() AS "uid"))) WITH CHECK (("user_id" = ( SELECT "auth"."uid"() AS "uid")));



CREATE POLICY "profiles_select" ON "lensers"."profiles" FOR SELECT USING ((("user_id" = ( SELECT "auth"."uid"() AS "uid")) OR (("status" = 'active'::"lensers"."lenser_status") AND ("deletion_requested_at" IS NULL) AND ("visibility" = ANY (ARRAY['public'::"lensers"."lenser_visibility", 'community'::"lensers"."lenser_visibility"]))) OR (("status" = 'active'::"lensers"."lenser_status") AND ("deletion_requested_at" IS NULL) AND ("visibility" = 'private'::"lensers"."lenser_visibility") AND (( SELECT "auth"."uid"() AS "uid") IS NOT NULL))));



CREATE POLICY "profiles_service_update" ON "lensers"."profiles" FOR UPDATE TO "service_role" USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text")) WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



ALTER TABLE "lensers"."relationships" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "relationships_select" ON "lensers"."relationships" FOR SELECT USING ((("source_profile_id" IN ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid")))) OR ("target_profile_id" IN ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))))));



ALTER TABLE "lensers"."social_links" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "social_links_owner_delete" ON "lensers"."social_links" FOR DELETE USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "social_links_owner_insert" ON "lensers"."social_links" FOR INSERT WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "social_links_owner_update" ON "lensers"."social_links" FOR UPDATE USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"()))) WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "social_links_public_select" ON "lensers"."social_links" FOR SELECT USING (true);



CREATE POLICY "social_links_service_role_all" ON "lensers"."social_links" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lensers"."tag_follows" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tag_follows_delete_self" ON "lensers"."tag_follows" FOR DELETE USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "tag_follows_insert_self" ON "lensers"."tag_follows" FOR INSERT WITH CHECK (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = ( SELECT "auth"."uid"() AS "uid"))
 LIMIT 1)));



CREATE POLICY "tag_follows_select_public" ON "lensers"."tag_follows" FOR SELECT USING (true);



CREATE POLICY "anon_select" ON "lenses"."lenses" FOR SELECT TO "anon" USING ((("visibility" = 'public'::"content"."visibility_enum") AND ("status" = 'published'::"content"."content_status") AND (EXISTS ( SELECT 1
   FROM "lensers"."profiles" "p"
  WHERE (("p"."id" = "lenses"."lenser_id") AND ("p"."status" = 'active'::"lensers"."lenser_status"))))));



CREATE POLICY "anon_select" ON "lenses"."version_parameters" FOR SELECT TO "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."versions" "v"
     JOIN "lenses"."lenses" "p" ON (("p"."id" = "v"."lens_id")))
  WHERE (("v"."id" = "version_parameters"."version_id") AND ("p"."visibility" = 'public'::"content"."visibility_enum") AND ("p"."status" = 'published'::"content"."content_status")))));



CREATE POLICY "anon_select_published" ON "lenses"."versions" FOR SELECT TO "anon" USING ((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "p"
  WHERE (("p"."id" = "versions"."lens_id") AND ("p"."visibility" = 'public'::"content"."visibility_enum") AND ("p"."status" = 'published'::"content"."content_status")))));



CREATE POLICY "authenticated users can read tools" ON "lenses"."tools" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "authenticated_delete" ON "lenses"."lenses" FOR DELETE TO "authenticated" USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "authenticated_insert" ON "lenses"."lenses" FOR INSERT TO "authenticated" WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "authenticated_insert" ON "lenses"."version_parameters" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("lenses"."versions" "v"
     JOIN "lenses"."lenses" "p" ON (("p"."id" = "v"."lens_id")))
  WHERE (("v"."id" = "version_parameters"."version_id") AND ("p"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("v"."status" = 'draft'::"content"."content_status")))));



CREATE POLICY "authenticated_insert" ON "lenses"."versions" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "p"
  WHERE (("p"."id" = "versions"."lens_id") AND ("p"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "authenticated_select" ON "lenses"."lenses" FOR SELECT TO "authenticated" USING (((("visibility" = ANY (ARRAY['public'::"content"."visibility_enum", 'community'::"content"."visibility_enum"])) AND ("status" = 'published'::"content"."content_status") AND (EXISTS ( SELECT 1
   FROM "lensers"."profiles" "p"
  WHERE (("p"."id" = "lenses"."lenser_id") AND ("p"."status" = 'active'::"lensers"."lenser_status"))))) OR ("lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "authenticated_select" ON "lenses"."version_parameters" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."versions" "v"
     JOIN "lenses"."lenses" "p" ON (("p"."id" = "v"."lens_id")))
  WHERE (("v"."id" = "version_parameters"."version_id") AND (("p"."lenser_id" = "lensers"."get_auth_lenser_id"()) OR (("p"."visibility" = ANY (ARRAY['public'::"content"."visibility_enum", 'community'::"content"."visibility_enum"])) AND ("p"."status" = 'published'::"content"."content_status")))))));



CREATE POLICY "authenticated_select" ON "lenses"."versions" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "p"
  WHERE (("p"."id" = "versions"."lens_id") AND (("p"."lenser_id" = "lensers"."get_auth_lenser_id"()) OR (("p"."visibility" = ANY (ARRAY['public'::"content"."visibility_enum", 'community'::"content"."visibility_enum"])) AND ("p"."status" = 'published'::"content"."content_status")))))));



CREATE POLICY "authenticated_update" ON "lenses"."lenses" FOR UPDATE TO "authenticated" USING (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"()))) WITH CHECK (("lensers"."is_active_lenser"(( SELECT "auth"."uid"() AS "uid")) AND ("lenser_id" = "lensers"."current_active_lenser_id"())));



CREATE POLICY "authenticated_update" ON "lenses"."version_parameters" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."versions" "v"
     JOIN "lenses"."lenses" "p" ON (("p"."id" = "v"."lens_id")))
  WHERE (("v"."id" = "version_parameters"."version_id") AND ("p"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("v"."status" = 'draft'::"content"."content_status")))));



CREATE POLICY "authenticated_update_draft" ON "lenses"."versions" FOR UPDATE TO "authenticated" USING ((("status" = 'draft'::"content"."content_status") AND (EXISTS ( SELECT 1
   FROM "lenses"."lenses" "p"
  WHERE (("p"."id" = "versions"."lens_id") AND ("p"."lenser_id" = "lensers"."get_auth_lenser_id"())))))) WITH CHECK (("status" = 'draft'::"content"."content_status"));



ALTER TABLE "lenses"."capability_index" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "capability_index_no_direct_write" ON "lenses"."capability_index" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "capability_index_visible_select" ON "lenses"."capability_index" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "capability_index"."content_hash") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."contract_channels" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "contract_channels_no_direct_write" ON "lenses"."contract_channels" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "contract_channels_visible_select" ON "lenses"."contract_channels" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "l"
  WHERE (("l"."id" = "contract_channels"."lens_id") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."contract_signatures" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "contract_signatures_no_direct_write" ON "lenses"."contract_signatures" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "contract_signatures_visible_select" ON "lenses"."contract_signatures" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "contract_signatures"."content_hash") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."contracts" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "contracts_no_direct_write" ON "lenses"."contracts" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "contracts_visible_select" ON "lenses"."contracts" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM "lenses"."lenses" "l"
  WHERE (("l"."id" = "contracts"."lens_id") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."dependency_edges" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "dependency_edges_no_direct_write" ON "lenses"."dependency_edges" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "dependency_edges_visible_select" ON "lenses"."dependency_edges" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "dependency_edges"."parent_content_hash") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."execution_records" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "execution_records_no_direct_write" ON "lenses"."execution_records" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "execution_records_owner_select" ON "lenses"."execution_records" FOR SELECT TO "authenticated" USING ((("principal_lenser_id" = "lensers"."get_auth_lenser_id"()) OR (EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "execution_records"."content_hash") AND ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "lens_steps_owner_delete" ON "lenses"."steps" FOR DELETE TO "authenticated" USING (("lens_id" IN ( SELECT "lenses"."id"
   FROM "lenses"."lenses"
  WHERE ("lenses"."lenser_id" = ( SELECT "auth"."uid"() AS "uid")))));



CREATE POLICY "lens_steps_owner_insert" ON "lenses"."steps" FOR INSERT TO "authenticated" WITH CHECK (("lens_id" IN ( SELECT "lenses"."id"
   FROM "lenses"."lenses"
  WHERE ("lenses"."lenser_id" = ( SELECT "auth"."uid"() AS "uid")))));



CREATE POLICY "lens_steps_owner_update" ON "lenses"."steps" FOR UPDATE TO "authenticated" USING (("lens_id" IN ( SELECT "lenses"."id"
   FROM "lenses"."lenses"
  WHERE ("lenses"."lenser_id" = ( SELECT "auth"."uid"() AS "uid"))))) WITH CHECK (("lens_id" IN ( SELECT "lenses"."id"
   FROM "lenses"."lenses"
  WHERE ("lenses"."lenser_id" = ( SELECT "auth"."uid"() AS "uid")))));



CREATE POLICY "lens_steps_read" ON "lenses"."steps" FOR SELECT USING (("lens_id" IN ( SELECT "lenses"."id"
   FROM "lenses"."lenses"
  WHERE (("lenses"."lenser_id" = ( SELECT "auth"."uid"() AS "uid")) OR (("lenses"."visibility" = ANY (ARRAY['public'::"content"."visibility_enum", 'community'::"content"."visibility_enum"])) AND ("lenses"."status" = 'published'::"content"."content_status"))))));



CREATE POLICY "lens_steps_service_role_all" ON "lenses"."steps" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "lenser deletes own parameter contents" ON "lenses"."version_parameter_contents" FOR DELETE USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "lenser inserts own parameter contents" ON "lenses"."version_parameter_contents" FOR INSERT WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "lenser reads own parameter contents" ON "lenses"."version_parameter_contents" FOR SELECT USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "lenser updates own parameter contents" ON "lenses"."version_parameter_contents" FOR UPDATE USING (("lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "lenses"."lenses" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."parameter_contracts" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "parameter_contracts_no_direct_write" ON "lenses"."parameter_contracts" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "parameter_contracts_visible_select" ON "lenses"."parameter_contracts" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "parameter_contracts"."content_hash") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."parameter_deprecations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "parameter_deprecations_no_direct_write" ON "lenses"."parameter_deprecations" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "parameter_deprecations_visible_select" ON "lenses"."parameter_deprecations" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "parameter_deprecations"."from_content_hash") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "phases_owner_write" ON "lenses"."workflow_phases" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_phases"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_phases"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "phases_read" ON "lenses"."workflow_phases" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_phases"."workflow_id") AND (("w"."visibility" = 'public'::"text") OR ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."schedule_calendars" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "schedule_calendars_delete" ON "lenses"."schedule_calendars" FOR DELETE TO "authenticated" USING ((("is_seed" = false) AND ("lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "schedule_calendars_insert" ON "lenses"."schedule_calendars" FOR INSERT TO "authenticated" WITH CHECK ((("is_seed" = false) AND ("lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "schedule_calendars_select" ON "lenses"."schedule_calendars" FOR SELECT TO "authenticated" USING ((("is_seed" = true) OR ("lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "schedule_calendars_update" ON "lenses"."schedule_calendars" FOR UPDATE TO "authenticated" USING ((("is_seed" = false) AND ("lenser_id" = "lensers"."get_auth_lenser_id"()))) WITH CHECK ((("is_seed" = false) AND ("lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "schedule_deny_direct_delete" ON "lenses"."workflow_schedules" FOR DELETE TO "authenticated" USING (false);



CREATE POLICY "schedule_deny_direct_insert" ON "lenses"."workflow_schedules" FOR INSERT TO "authenticated" WITH CHECK (false);



CREATE POLICY "schedule_deny_direct_update" ON "lenses"."workflow_schedules" FOR UPDATE TO "authenticated" USING (false);



CREATE POLICY "schedule_owner_select" ON "lenses"."workflow_schedules" FOR SELECT TO "authenticated" USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = "lensers"."get_auth_lenser_id"()))));



ALTER TABLE "lenses"."security_scopes" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "security_scopes_no_direct_write" ON "lenses"."security_scopes" TO "authenticated", "anon" USING (false) WITH CHECK (false);



CREATE POLICY "security_scopes_visible_select" ON "lenses"."security_scopes" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."contracts" "c"
     JOIN "lenses"."lenses" "l" ON (("l"."id" = "c"."lens_id")))
  WHERE (("c"."content_hash" = "security_scopes"."content_hash") AND (("l"."visibility" = 'public'::"content"."visibility_enum") OR ("l"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "service_role_all" ON "lenses"."lenses" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role_all" ON "lenses"."version_parameters" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role_all" ON "lenses"."versions" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."steps" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tasks_owner_write" ON "lenses"."workflow_tasks" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_tasks"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_tasks"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "tasks_read" ON "lenses"."workflow_tasks" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_tasks"."workflow_id") AND (("w"."visibility" = 'public'::"text") OR ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



ALTER TABLE "lenses"."tools" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."tutorial_completions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tutorial_completions_owner_read" ON "lenses"."tutorial_completions" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "lenses"."version_parameter_contents" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."version_parameters" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."versions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "wf_edges_owner_delete" ON "lenses"."workflow_edges" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_edges"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wf_edges_owner_insert" ON "lenses"."workflow_edges" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_edges"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wf_edges_owner_update" ON "lenses"."workflow_edges" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_edges"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_edges"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wf_edges_read" ON "lenses"."workflow_edges" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_edges"."workflow_id") AND (("w"."visibility" = 'public'::"text") OR ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "wf_nodes_owner_delete" ON "lenses"."workflow_nodes" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_nodes"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wf_nodes_owner_insert" ON "lenses"."workflow_nodes" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_nodes"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wf_nodes_owner_update" ON "lenses"."workflow_nodes" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_nodes"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_nodes"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wf_nodes_read" ON "lenses"."workflow_nodes" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_nodes"."workflow_id") AND (("w"."visibility" = 'public'::"text") OR ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))));



CREATE POLICY "wf_runs_owner_read" ON "lenses"."workflow_runs" FOR SELECT USING ((("triggered_by" = "lensers"."get_auth_lenser_id"()) OR (EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_runs"."workflow_id") AND ("w"."visibility" = 'public'::"text"))))));



CREATE POLICY "wnr_owner_read" ON "lenses"."workflow_node_results" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflow_runs" "wr"
  WHERE (("wr"."id" = "workflow_node_results"."run_id") AND (("wr"."triggered_by" = "lensers"."get_auth_lenser_id"()) OR (EXISTS ( SELECT 1
           FROM "lenses"."workflows" "w"
          WHERE (("w"."id" = "wr"."workflow_id") AND ("w"."visibility" = 'public'::"text")))))))));



CREATE POLICY "workflow_dlq_service_all" ON "lenses"."workflow_run_dead_letters" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_edges" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_node_results" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_nodes" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_phases" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_run_chains" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_run_chains_owner" ON "lenses"."workflow_run_chains" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflow_runs" "r"
  WHERE (("r"."id" = "r"."parent_run_id") AND ("r"."triggered_by" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "workflow_run_chains_service" ON "lenses"."workflow_run_chains" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_run_dead_letters" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_run_events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_run_events_select" ON "lenses"."workflow_run_events" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."workflow_runs" "r"
     JOIN "lenses"."workflows" "w" ON (("w"."id" = "r"."workflow_id")))
  WHERE (("r"."id" = "workflow_run_events"."run_id") AND (("r"."triggered_by" = "lensers"."get_auth_lenser_id"()) OR ("w"."visibility" = 'public'::"text"))))));



CREATE POLICY "workflow_run_events_service_all" ON "lenses"."workflow_run_events" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_run_provenance" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_run_provenance_select" ON "lenses"."workflow_run_provenance" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."workflow_runs" "r"
     JOIN "lenses"."workflows" "w" ON (("w"."id" = "r"."workflow_id")))
  WHERE ((("r"."id" = "workflow_run_provenance"."target_run_id") OR ("r"."id" = "workflow_run_provenance"."source_run_id")) AND (("r"."triggered_by" = "lensers"."get_auth_lenser_id"()) OR ("w"."visibility" = 'public'::"text"))))));



CREATE POLICY "workflow_run_provenance_service_all" ON "lenses"."workflow_run_provenance" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_run_tags" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_run_tags_select" ON "lenses"."workflow_run_tags" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("lenses"."workflow_runs" "r"
     JOIN "lenses"."workflows" "w" ON (("w"."id" = "r"."workflow_id")))
  WHERE (("r"."id" = "workflow_run_tags"."run_id") AND (("r"."triggered_by" = "lensers"."get_auth_lenser_id"()) OR ("w"."visibility" = 'public'::"text"))))));



CREATE POLICY "workflow_run_tags_service_all" ON "lenses"."workflow_run_tags" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_runs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_runs_owner_select" ON "lenses"."workflow_runs" FOR SELECT TO "authenticated" USING (("triggered_by" = "auth"."uid"()));



CREATE POLICY "workflow_runs_service_all" ON "lenses"."workflow_runs" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_schedules" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_schedules_owner" ON "lenses"."workflow_schedules" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_schedules"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



ALTER TABLE "lenses"."workflow_tasks" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_triggers" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_triggers_owner" ON "lenses"."workflow_triggers" TO "authenticated" USING (("owner_id" = "auth"."uid"())) WITH CHECK (("owner_id" = "auth"."uid"()));



CREATE POLICY "workflow_triggers_service" ON "lenses"."workflow_triggers" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "lenses"."workflow_version_edges" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_version_nodes" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflow_versions" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "lenses"."workflows" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflows_owner_delete" ON "lenses"."workflows" FOR DELETE USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "workflows_owner_insert" ON "lenses"."workflows" FOR INSERT WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "workflows_owner_update" ON "lenses"."workflows" FOR UPDATE USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "workflows_public_read" ON "lenses"."workflows" FOR SELECT USING (((("visibility" = 'public'::"text") AND (EXISTS ( SELECT 1
   FROM "lensers"."profiles" "p"
  WHERE (("p"."id" = "workflows"."lenser_id") AND ("p"."status" = 'active'::"lensers"."lenser_status"))))) OR ("lenser_id" = "lensers"."get_auth_lenser_id"())));



CREATE POLICY "wv_anon_select" ON "lenses"."workflow_versions" FOR SELECT TO "anon" USING ((("status" = 'published'::"text") AND (EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_versions"."workflow_id") AND ("w"."visibility" = 'public'::"text"))))));



CREATE POLICY "wv_owner_all" ON "lenses"."workflow_versions" TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_versions"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"()))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_versions"."workflow_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wv_public_select" ON "lenses"."workflow_versions" FOR SELECT TO "authenticated" USING ((("status" = 'published'::"text") AND (EXISTS ( SELECT 1
   FROM "lenses"."workflows" "w"
  WHERE (("w"."id" = "workflow_versions"."workflow_id") AND ("w"."visibility" = 'public'::"text"))))));



CREATE POLICY "wv_service_all" ON "lenses"."workflow_versions" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "wve_owner_insert" ON "lenses"."workflow_version_edges" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("lenses"."workflow_versions" "wv"
     JOIN "lenses"."workflows" "w" ON (("w"."id" = "wv"."workflow_id")))
  WHERE (("wv"."id" = "workflow_version_edges"."workflow_version_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wve_select_via_version" ON "lenses"."workflow_version_edges" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflow_versions" "wv"
  WHERE ("wv"."id" = "workflow_version_edges"."workflow_version_id"))));



CREATE POLICY "wve_service_all" ON "lenses"."workflow_version_edges" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "wvn_owner_insert" ON "lenses"."workflow_version_nodes" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("lenses"."workflow_versions" "wv"
     JOIN "lenses"."workflows" "w" ON (("w"."id" = "wv"."workflow_id")))
  WHERE (("wv"."id" = "workflow_version_nodes"."workflow_version_id") AND ("w"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "wvn_select_via_version" ON "lenses"."workflow_version_nodes" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM "lenses"."workflow_versions" "wv"
  WHERE ("wv"."id" = "workflow_version_nodes"."workflow_version_id"))));



CREATE POLICY "wvn_service_all" ON "lenses"."workflow_version_nodes" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "anon_select_public" ON "media"."objects" FOR SELECT TO "anon" USING (("visibility" = 'public'::"text"));



CREATE POLICY "anon_select_public_attachments" ON "media"."attachments" FOR SELECT TO "anon" USING ((EXISTS ( SELECT 1
   FROM "media"."objects" "o"
  WHERE (("o"."id" = "attachments"."object_id") AND ("o"."visibility" = 'public'::"text")))));



ALTER TABLE "media"."attachments" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "authenticated_delete_attachments" ON "media"."attachments" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "media"."objects" "o"
  WHERE (("o"."id" = "attachments"."object_id") AND ("o"."owner_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "authenticated_delete_own" ON "media"."objects" FOR DELETE TO "authenticated" USING (("owner_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "authenticated_insert_attachments" ON "media"."attachments" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "media"."objects" "o"
  WHERE (("o"."id" = "attachments"."object_id") AND ("o"."owner_lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "authenticated_insert_own" ON "media"."objects" FOR INSERT TO "authenticated" WITH CHECK ((("owner_lenser_id" = "lensers"."get_auth_lenser_id"()) AND "tenancy"."is_workspace_member"("workspace_id")));



CREATE POLICY "authenticated_select_attachments" ON "media"."attachments" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "media"."objects" "o"
  WHERE (("o"."id" = "attachments"."object_id") AND (("o"."owner_lenser_id" = "lensers"."get_auth_lenser_id"()) OR ("o"."visibility" = 'public'::"text") OR "tenancy"."is_workspace_member"("o"."workspace_id"))))));



CREATE POLICY "authenticated_select_own_or_public" ON "media"."objects" FOR SELECT TO "authenticated" USING ((("owner_lenser_id" = "lensers"."get_auth_lenser_id"()) OR ("visibility" = 'public'::"text") OR "tenancy"."is_workspace_member"("workspace_id")));



CREATE POLICY "authenticated_update_own" ON "media"."objects" FOR UPDATE TO "authenticated" USING (("owner_lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "media"."objects" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "organizations"."activity_log" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "activity_log_insert_admin" ON "organizations"."activity_log" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "activity_log"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "activity_log_select_member" ON "organizations"."activity_log" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "activity_log"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "activity_log_service_all" ON "organizations"."activity_log" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "activity_log_update_admin" ON "organizations"."activity_log" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "activity_log"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



ALTER TABLE "organizations"."addresses" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "addresses_insert_admin" ON "organizations"."addresses" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM ("organizations"."companies" "c"
     JOIN "organizations"."members" "m" ON (("m"."org_id" = "c"."org_id")))
  WHERE (("c"."id" = "addresses"."company_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "addresses_select_member" ON "organizations"."addresses" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("organizations"."companies" "c"
     JOIN "organizations"."members" "m" ON (("m"."org_id" = "c"."org_id")))
  WHERE (("c"."id" = "addresses"."company_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "addresses_service_all" ON "organizations"."addresses" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "addresses_update_admin" ON "organizations"."addresses" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM ("organizations"."companies" "c"
     JOIN "organizations"."members" "m" ON (("m"."org_id" = "c"."org_id")))
  WHERE (("c"."id" = "addresses"."company_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



ALTER TABLE "organizations"."audit_logs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "audit_logs_insert_admin" ON "organizations"."audit_logs" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "audit_logs"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "audit_logs_select_member" ON "organizations"."audit_logs" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "audit_logs"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "audit_logs_service_all" ON "organizations"."audit_logs" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "organizations"."companies" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "companies_insert_admin" ON "organizations"."companies" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "companies"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "companies_select_member" ON "organizations"."companies" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "companies"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "companies_service_all" ON "organizations"."companies" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "companies_update_admin" ON "organizations"."companies" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "companies"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



ALTER TABLE "organizations"."members" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "members_manage_delete" ON "organizations"."members" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "members"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "members_manage_insert" ON "organizations"."members" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "members"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "members_manage_update" ON "organizations"."members" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "members"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"])))))) WITH CHECK ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "members"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "members_select_member" ON "organizations"."members" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "members"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "members_service_all" ON "organizations"."members" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "organizations"."organizations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "organizations_insert_own" ON "organizations"."organizations" FOR INSERT TO "authenticated" WITH CHECK (("created_by" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "organizations_select_member" ON "organizations"."organizations" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "organizations"."id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "organizations_service_all" ON "organizations"."organizations" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "organizations_update_admin" ON "organizations"."organizations" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "organizations"."id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



ALTER TABLE "organizations"."preferences" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "preferences_insert_admin" ON "organizations"."preferences" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "preferences"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



CREATE POLICY "preferences_select_member" ON "organizations"."preferences" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "preferences"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "preferences_service_all" ON "organizations"."preferences" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "preferences_update_admin" ON "organizations"."preferences" FOR UPDATE TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "m"
  WHERE (("m"."org_id" = "preferences"."org_id") AND ("m"."lenser_id" = "lensers"."get_auth_lenser_id"()) AND ("m"."role" = ANY (ARRAY['owner'::"organizations"."member_role_enum", 'admin'::"organizations"."member_role_enum"]))))));



ALTER TABLE "platform"."api_worker_heartbeats" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "heartbeats_service_all" ON "platform"."api_worker_heartbeats" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "platform"."system_flags" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "system_flags_deny_write" ON "platform"."system_flags" TO "authenticated" USING (false) WITH CHECK (false);



CREATE POLICY "system_flags_read" ON "platform"."system_flags" FOR SELECT TO "authenticated" USING (true);



ALTER TABLE "platform"."tool_invocation_logs" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "tool_log_deny_direct_insert" ON "platform"."tool_invocation_logs" FOR INSERT TO "authenticated" WITH CHECK (false);



CREATE POLICY "tool_log_deny_direct_update" ON "platform"."tool_invocation_logs" FOR UPDATE TO "authenticated" USING (false);



CREATE POLICY "tool_log_owner_select" ON "platform"."tool_invocation_logs" FOR SELECT TO "authenticated" USING (("ai_lenser_id" IN ( SELECT "al"."id"
   FROM "agents"."ai_lensers" "al"
  WHERE ("al"."profile_id" = "lensers"."get_auth_lenser_id"()))));



CREATE POLICY "notif_prefs_owner_insert" ON "public"."notification_preferences" FOR INSERT TO "authenticated" WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notif_prefs_owner_select" ON "public"."notification_preferences" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notif_prefs_owner_update" ON "public"."notification_preferences" FOR UPDATE TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "public"."notification_aggregates" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "notification_aggregates_owner_select" ON "public"."notification_aggregates" FOR SELECT TO "authenticated" USING (("recipient_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notification_aggregates_service_all" ON "public"."notification_aggregates" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "public"."notification_preferences" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "public"."notifications" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "notifications_owner_select" ON "public"."notifications" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notifications_owner_update" ON "public"."notifications" FOR UPDATE TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notifications_service_insert" ON "public"."notifications" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "uoc_owner_select" ON "public"."user_oauth_connections" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



ALTER TABLE "public"."user_oauth_connections" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "wic_owner_delete" ON "public"."workflow_integration_credentials" FOR DELETE USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



CREATE POLICY "wic_owner_insert" ON "public"."workflow_integration_credentials" FOR INSERT WITH CHECK (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



CREATE POLICY "wic_owner_select" ON "public"."workflow_integration_credentials" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



CREATE POLICY "wic_owner_update" ON "public"."workflow_integration_credentials" FOR UPDATE USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



ALTER TABLE "public"."workflow_integration_credentials" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "public"."workflow_kv_store" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_kv_store_owner_delete" ON "public"."workflow_kv_store" FOR DELETE USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_kv_store_owner_insert" ON "public"."workflow_kv_store" FOR INSERT WITH CHECK (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_kv_store_owner_select" ON "public"."workflow_kv_store" FOR SELECT USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



ALTER TABLE "public"."workflow_webhook_triggers" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_webhook_triggers_owner_delete" ON "public"."workflow_webhook_triggers" FOR DELETE USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_webhook_triggers_owner_insert" ON "public"."workflow_webhook_triggers" FOR INSERT WITH CHECK (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_webhook_triggers_owner_select" ON "public"."workflow_webhook_triggers" FOR SELECT USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



ALTER TABLE "reputation"."contender_ratings" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "contender_ratings_authenticated_read" ON "reputation"."contender_ratings" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "contender_ratings_service_write" ON "reputation"."contender_ratings" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "reputation"."elo_battle_log" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "elo_battle_log_service_role_all" ON "reputation"."elo_battle_log" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "reputation"."judge_calibrations" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "judge_calibrations_authenticated_read" ON "reputation"."judge_calibrations" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "judge_calibrations_service_write" ON "reputation"."judge_calibrations" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "reputation"."lenser_scores" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "lenser_scores_authenticated_read" ON "reputation"."lenser_scores" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "lenser_scores_service_write" ON "reputation"."lenser_scores" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role_all" ON "reputation"."contender_ratings" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role_all" ON "reputation"."lenser_scores" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "reputation"."vote_risk_scores" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "vote_risk_scores_service_only" ON "reputation"."vote_risk_scores" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "status"."components" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "status"."daily_snapshots" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "status"."incident_updates" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "status"."incidents" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "status"."maintenance_windows" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "public read components" ON "status"."components" FOR SELECT TO "authenticated", "anon" USING ((("is_public" = true) AND (EXISTS ( SELECT 1
   FROM "status"."services" "s"
  WHERE (("s"."id" = "components"."service_id") AND ("s"."is_public" = true))))));



CREATE POLICY "public read daily_snapshots" ON "status"."daily_snapshots" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "public read incident_updates" ON "status"."incident_updates" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
   FROM ("status"."incidents" "i"
     LEFT JOIN "status"."services" "s" ON (("s"."id" = "i"."service_id")))
  WHERE (("i"."id" = "incident_updates"."incident_id") AND ("i"."published" = true) AND (("i"."service_id" IS NULL) OR ("s"."is_public" = true))))));



CREATE POLICY "public read incidents" ON "status"."incidents" FOR SELECT TO "authenticated", "anon" USING ((("published" = true) AND (("service_id" IS NULL) OR (EXISTS ( SELECT 1
   FROM "status"."services" "s"
  WHERE (("s"."id" = "incidents"."service_id") AND ("s"."is_public" = true)))))));



CREATE POLICY "public read maintenance_windows" ON "status"."maintenance_windows" FOR SELECT TO "authenticated", "anon" USING ((("published" = true) AND (("service_id" IS NULL) OR (EXISTS ( SELECT 1
   FROM "status"."services" "s"
  WHERE (("s"."id" = "maintenance_windows"."service_id") AND ("s"."is_public" = true)))))));



CREATE POLICY "public read services" ON "status"."services" FOR SELECT TO "authenticated", "anon" USING (("is_public" = true));



CREATE POLICY "public read uptime_rollups" ON "status"."uptime_rollups" FOR SELECT TO "authenticated", "anon" USING (true);



CREATE POLICY "service_role full access components" ON "status"."components" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role full access daily_snapshots" ON "status"."daily_snapshots" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role full access incident_updates" ON "status"."incident_updates" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role full access incidents" ON "status"."incidents" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role full access maintenance_windows" ON "status"."maintenance_windows" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role full access services" ON "status"."services" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "service_role full access uptime_rollups" ON "status"."uptime_rollups" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "status"."services" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "status"."uptime_rollups" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "admin_delete_members" ON "tenancy"."workspace_members" FOR DELETE TO "authenticated" USING ("tenancy"."is_workspace_admin"("workspace_id"));



CREATE POLICY "admin_insert_members" ON "tenancy"."workspace_members" FOR INSERT TO "authenticated" WITH CHECK ("tenancy"."is_workspace_admin"("workspace_id"));



CREATE POLICY "admin_update_workspace" ON "tenancy"."workspaces" FOR UPDATE TO "authenticated" USING ("tenancy"."is_workspace_admin"("id"));



CREATE POLICY "authenticated_insert_workspace" ON "tenancy"."workspaces" FOR INSERT TO "authenticated" WITH CHECK (("owner_lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "members_select_own_workspaces" ON "tenancy"."workspaces" FOR SELECT TO "authenticated" USING ("tenancy"."is_workspace_member"("id"));



CREATE POLICY "members_select_same_workspace" ON "tenancy"."workspace_members" FOR SELECT TO "authenticated" USING ("tenancy"."is_workspace_member"("workspace_id"));



ALTER TABLE "tenancy"."workspace_members" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "tenancy"."workspaces" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "wallet"."accounts" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "accounts_service_all" ON "wallet"."accounts" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "wallet"."credit_valuation_policy" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "credit_valuation_service_all" ON "wallet"."credit_valuation_policy" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "org_accounts_select_member" ON "wallet"."organization_accounts" FOR SELECT TO "authenticated" USING ((EXISTS ( SELECT 1
   FROM "organizations"."members" "om"
  WHERE (("om"."org_id" = "organization_accounts"."org_id") AND ("om"."lenser_id" = "lensers"."get_auth_lenser_id"())))));



CREATE POLICY "org_accounts_service_all" ON "wallet"."organization_accounts" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "wallet"."organization_accounts" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "wallet"."pending_charges" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "pending_charges_service_all" ON "wallet"."pending_charges" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "wallet"."spending_limits" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "spending_limits_service_all" ON "wallet"."spending_limits" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "wallet"."transactions" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "transactions_service_insert" ON "wallet"."transactions" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "transactions_service_select" ON "wallet"."transactions" FOR SELECT TO "service_role" USING (true);



CREATE POLICY "Authenticated can read xp levels" ON "xp"."levels" FOR SELECT TO "authenticated" USING (true);



ALTER TABLE "xp"."apps" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."contributions" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."difficulty_multipliers" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."event_verifications" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."events" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "lenser_level_ups_select_own" ON "xp"."level_ups" FOR SELECT USING (("lenser_id" = ( SELECT "auth"."uid"() AS "uid")));



ALTER TABLE "xp"."level_ups" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."levels" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."monthly_rollup" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."policy" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."rules" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."season_totals" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."seasons" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."streaks" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "xp"."totals" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "xp_apps_manage_service" ON "xp"."apps" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "xp_apps_select_public" ON "xp"."apps" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "xp_contributions_manage_service" ON "xp"."contributions" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "xp_contributions_select_own" ON "xp"."contributions" FOR SELECT TO "authenticated" USING (("lenser_id" = ( SELECT "auth"."uid"() AS "uid")));



CREATE POLICY "xp_difficulty_multipliers_manage_service" ON "xp"."difficulty_multipliers" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "xp_difficulty_multipliers_select_public" ON "xp"."difficulty_multipliers" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "xp_event_verifications_select_own" ON "xp"."event_verifications" FOR SELECT USING ((EXISTS ( SELECT 1
   FROM "xp"."events" "e"
  WHERE (("e"."id" = "event_verifications"."event_id") AND ("e"."lenser_id" = ( SELECT "auth"."uid"() AS "uid"))))));



CREATE POLICY "xp_events_select_own" ON "xp"."events" FOR SELECT USING (("lenser_id" = ( SELECT "auth"."uid"() AS "uid")));



CREATE POLICY "xp_levels_service_delete" ON "xp"."levels" FOR DELETE USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "xp_levels_service_update" ON "xp"."levels" FOR UPDATE USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text")) WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "xp_levels_service_write" ON "xp"."levels" FOR INSERT WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "xp_monthly_rollup_select_own" ON "xp"."monthly_rollup" FOR SELECT USING (("lenser_id" = ( SELECT "auth"."uid"() AS "uid")));



CREATE POLICY "xp_policy_service_only" ON "xp"."policy" USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text")) WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "xp_rules_service_only" ON "xp"."rules" USING ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text")) WITH CHECK ((( SELECT "auth"."role"() AS "role") = 'service_role'::"text"));



CREATE POLICY "xp_season_totals_authenticated_select" ON "xp"."season_totals" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "xp_seasons_select_authenticated" ON "xp"."seasons" FOR SELECT TO "authenticated" USING (true);



CREATE POLICY "xp_seasons_write_service_only" ON "xp"."seasons" TO "service_role" USING (true) WITH CHECK (true);



CREATE POLICY "xp_streaks_select_own" ON "xp"."streaks" FOR SELECT USING (("lenser_id" = ( SELECT "auth"."uid"() AS "uid")));



CREATE POLICY "xp_totals_authenticated_select" ON "xp"."totals" FOR SELECT TO "authenticated" USING (true);





ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres";






ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "battles"."battles";



ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "battles"."comments";



ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "battles"."global_messages";



ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "battles"."vote_aggregates";



ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "lenses"."workflow_node_results";



ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "lenses"."workflow_runs";



ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "public"."notifications";



GRANT USAGE ON SCHEMA "agents" TO "anon";
GRANT USAGE ON SCHEMA "agents" TO "authenticated";
GRANT USAGE ON SCHEMA "agents" TO "service_role";



GRANT USAGE ON SCHEMA "ai" TO "anon";
GRANT USAGE ON SCHEMA "ai" TO "authenticated";



GRANT USAGE ON SCHEMA "analytics" TO "service_role";
GRANT USAGE ON SCHEMA "analytics" TO "authenticated";



GRANT USAGE ON SCHEMA "audit" TO "authenticated";
GRANT USAGE ON SCHEMA "audit" TO "service_role";



GRANT USAGE ON SCHEMA "automation" TO "authenticated";
GRANT USAGE ON SCHEMA "automation" TO "service_role";



GRANT USAGE ON SCHEMA "battles" TO "anon";
GRANT USAGE ON SCHEMA "battles" TO "authenticated";
GRANT USAGE ON SCHEMA "battles" TO "service_role";



GRANT USAGE ON SCHEMA "billing" TO "service_role";



GRANT USAGE ON SCHEMA "connectors" TO "authenticated";
GRANT USAGE ON SCHEMA "connectors" TO "service_role";



GRANT USAGE ON SCHEMA "content" TO "authenticated";
GRANT USAGE ON SCHEMA "content" TO "anon";



GRANT USAGE ON SCHEMA "core" TO "anon";
GRANT USAGE ON SCHEMA "core" TO "authenticated";
GRANT USAGE ON SCHEMA "core" TO "service_role";






GRANT USAGE ON SCHEMA "devices" TO "authenticated";



GRANT USAGE ON SCHEMA "execution" TO "anon";
GRANT USAGE ON SCHEMA "execution" TO "authenticated";
GRANT USAGE ON SCHEMA "execution" TO "service_role";



GRANT USAGE ON SCHEMA "integrations" TO "authenticated";
GRANT USAGE ON SCHEMA "integrations" TO "anon";
GRANT USAGE ON SCHEMA "integrations" TO "service_role";



GRANT USAGE ON SCHEMA "internal" TO "service_role";



GRANT USAGE ON SCHEMA "lensers" TO "anon";
GRANT USAGE ON SCHEMA "lensers" TO "authenticated";
GRANT USAGE ON SCHEMA "lensers" TO "service_role";



GRANT USAGE ON SCHEMA "lenses" TO "authenticated";
GRANT USAGE ON SCHEMA "lenses" TO "service_role";



GRANT USAGE ON SCHEMA "media" TO "anon";
GRANT USAGE ON SCHEMA "media" TO "authenticated";
GRANT USAGE ON SCHEMA "media" TO "service_role";






GRANT USAGE ON SCHEMA "organizations" TO "anon";
GRANT USAGE ON SCHEMA "organizations" TO "authenticated";
GRANT USAGE ON SCHEMA "organizations" TO "service_role";



GRANT USAGE ON SCHEMA "public" TO "postgres";
GRANT USAGE ON SCHEMA "public" TO "anon";
GRANT USAGE ON SCHEMA "public" TO "authenticated";
GRANT USAGE ON SCHEMA "public" TO "service_role";



GRANT USAGE ON SCHEMA "reputation" TO "anon";
GRANT USAGE ON SCHEMA "reputation" TO "authenticated";
GRANT USAGE ON SCHEMA "reputation" TO "service_role";



GRANT USAGE ON SCHEMA "status" TO "anon";
GRANT USAGE ON SCHEMA "status" TO "authenticated";
GRANT USAGE ON SCHEMA "status" TO "service_role";



GRANT USAGE ON SCHEMA "tenancy" TO "anon";
GRANT USAGE ON SCHEMA "tenancy" TO "authenticated";
GRANT USAGE ON SCHEMA "tenancy" TO "service_role";



GRANT USAGE ON SCHEMA "wallet" TO "service_role";



GRANT USAGE ON SCHEMA "xp" TO "service_role";
GRANT USAGE ON SCHEMA "xp" TO "anon";
GRANT USAGE ON SCHEMA "xp" TO "authenticated";



GRANT ALL ON TYPE "ai"."key_scope_enum" TO "authenticated";
GRANT ALL ON TYPE "ai"."key_scope_enum" TO "anon";



GRANT ALL ON TYPE "ai"."key_status_enum" TO "authenticated";
GRANT ALL ON TYPE "ai"."key_status_enum" TO "anon";



GRANT ALL ON TYPE "content"."payment_method_enum" TO "authenticated";
GRANT ALL ON TYPE "content"."payment_method_enum" TO "anon";



GRANT ALL ON TYPE "content"."reaction_enum" TO "authenticated";













































GRANT ALL ON FUNCTION "agents"."can_manage_ai_lenser"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "agents"."can_manage_ai_lenser"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "agents"."fn_agent_action"("p_ai_lenser_id" "uuid", "p_action_type" "text", "p_context_type" "text", "p_context_id" "uuid", "p_metadata" "jsonb") TO "authenticated";



REVOKE ALL ON FUNCTION "agents"."fn_build_lenser_prompt_context"("p_ai_lenser_id" "uuid", "p_scope" "text", "p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_build_lenser_prompt_context"("p_ai_lenser_id" "uuid", "p_scope" "text", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "agents"."fn_bulk_approve"("p_filters" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_bulk_approve"("p_filters" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "agents"."fn_bulk_approve"("p_filters" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "agents"."fn_claim_team_run"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_claim_team_run"("p_worker_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "agents"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "agents"."fn_gateway_approve_device"("p_device_id" "uuid") TO "authenticated";



GRANT SELECT ON TABLE "agents"."gateway_commands" TO "authenticated";



GRANT ALL ON FUNCTION "agents"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "agents"."fn_get_team_member_role"("p_team_run_id" "uuid", "p_agent_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_get_team_member_role"("p_team_run_id" "uuid", "p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "agents"."fn_get_team_member_role"("p_team_run_id" "uuid", "p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "agents"."fn_has_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text") TO "authenticated";
GRANT ALL ON FUNCTION "agents"."fn_has_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text") TO "service_role";



GRANT ALL ON FUNCTION "agents"."fn_list_gateway_devices"("p_limit" integer) TO "authenticated";



REVOKE ALL ON FUNCTION "agents"."fn_merge_shared_scratchpad"("p_team_run_id" "uuid", "p_patch" "jsonb", "p_expected_version" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_merge_shared_scratchpad"("p_team_run_id" "uuid", "p_patch" "jsonb", "p_expected_version" integer) TO "authenticated";



REVOKE ALL ON FUNCTION "agents"."fn_node_requires_review"("p_team_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_node_requires_review"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "agents"."fn_node_requires_review"("p_team_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "agents"."fn_purge_stale_blocked_team_runs"("p_max_age" interval) FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_purge_stale_blocked_team_runs"("p_max_age" interval) TO "service_role";



REVOKE ALL ON FUNCTION "agents"."fn_send_team_message"("p_team_run_id" "uuid", "p_from_agent_id" "uuid", "p_kind" "text", "p_to_agent_id" "uuid", "p_payload" "jsonb", "p_parent_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_send_team_message"("p_team_run_id" "uuid", "p_from_agent_id" "uuid", "p_kind" "text", "p_to_agent_id" "uuid", "p_payload" "jsonb", "p_parent_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "agents"."fn_start_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_inputs" "jsonb", "p_policy" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "agents"."fn_start_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_inputs" "jsonb", "p_policy" "text") TO "service_role";



GRANT ALL ON FUNCTION "agents"."fn_trigger_post_run_evaluations"("p_workflow_id" "uuid", "p_team_run_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "agents"."has_agent_scope"("p_ai_lenser_id" "uuid", "p_scope" "text") TO "authenticated";
GRANT ALL ON FUNCTION "agents"."has_agent_scope"("p_ai_lenser_id" "uuid", "p_scope" "text") TO "service_role";



REVOKE ALL ON FUNCTION "ai"."fn_decrypt_api_key"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "ai"."fn_decrypt_api_key"("p_key_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "ai"."fn_get_my_api_keys"() FROM PUBLIC;
GRANT ALL ON FUNCTION "ai"."fn_get_my_api_keys"() TO "authenticated";



REVOKE ALL ON FUNCTION "ai"."fn_revoke_api_key"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "ai"."fn_revoke_api_key"("p_key_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "ai"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "ai"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "authenticated";



GRANT ALL ON FUNCTION "analytics"."fn_aggregate_cost_daily"("p_date" "date") TO "service_role";



GRANT ALL ON FUNCTION "analytics"."fn_aggregate_eval_quality_daily"("p_date" "date") TO "service_role";



GRANT ALL ON FUNCTION "analytics"."fn_aggregate_workflow_perf_daily"("p_date" "date") TO "service_role";



GRANT ALL ON FUNCTION "analytics"."log_tag_view"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid", "p_user_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "analytics"."protect_feedback_system_fields"() FROM PUBLIC;
GRANT ALL ON FUNCTION "analytics"."protect_feedback_system_fields"() TO "authenticated";
GRANT ALL ON FUNCTION "analytics"."protect_feedback_system_fields"() TO "service_role";



REVOKE ALL ON FUNCTION "analytics"."set_feedback_user_id"() FROM PUBLIC;
GRANT ALL ON FUNCTION "analytics"."set_feedback_user_id"() TO "authenticated";
GRANT ALL ON FUNCTION "analytics"."set_feedback_user_id"() TO "service_role";



REVOKE ALL ON FUNCTION "audit"."fn_append_gateway_event"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."fn_append_gateway_event"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "audit"."fn_append_gateway_event_safe"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."fn_append_gateway_event_safe"("p_event_type" "text", "p_lenser_id" "uuid", "p_device_id" "uuid", "p_payload" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "audit"."fn_assert_webhook_signing_state"() FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."fn_assert_webhook_signing_state"() TO "service_role";



REVOKE ALL ON FUNCTION "audit"."fn_chain_verify"("p_lenser_id" "uuid", "p_chain_kind" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."fn_chain_verify"("p_lenser_id" "uuid", "p_chain_kind" "text") TO "authenticated";
GRANT ALL ON FUNCTION "audit"."fn_chain_verify"("p_lenser_id" "uuid", "p_chain_kind" "text") TO "service_role";



REVOKE ALL ON FUNCTION "audit"."fn_cron_dispatch_webhook_outbox"() FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."fn_cron_dispatch_webhook_outbox"() TO "service_role";



GRANT ALL ON FUNCTION "audit"."fn_dispatch_webhook_outbox"("p_batch" integer) TO "service_role";



REVOKE ALL ON FUNCTION "audit"."log_event"("p_event_type" "text", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_actor_lenser_id" "uuid", "p_actor_actor_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."log_event"("p_event_type" "text", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_actor_lenser_id" "uuid", "p_actor_actor_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") TO "service_role";



REVOKE ALL ON FUNCTION "audit"."log_event_v2"("p_event_type" "text", "p_actor_type" "text", "p_actor_id" "uuid", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "audit"."log_event_v2"("p_event_type" "text", "p_actor_type" "text", "p_actor_id" "uuid", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") TO "service_role";
GRANT ALL ON FUNCTION "audit"."log_event_v2"("p_event_type" "text", "p_actor_type" "text", "p_actor_id" "uuid", "p_entity_schema" "text", "p_entity_table" "text", "p_entity_id" "uuid", "p_payload" "jsonb", "p_severity" "text", "p_ip_hash" "text", "p_user_agent_hash" "text") TO "authenticated";



REVOKE ALL ON FUNCTION "authz"."fn_approve_device_request"("p_user_code" "text") FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_generate_user_code"() FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_list_developer_tokens"() FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_request_device_login"("p_request_ttl_minutes" integer) FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_revoke_developer_token"("p_token_id" "uuid") FROM PUBLIC;



REVOKE ALL ON FUNCTION "authz"."fn_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") FROM PUBLIC;



REVOKE ALL ON FUNCTION "automation"."fn_check_filter_keys"("p_filter" "jsonb") FROM PUBLIC;



REVOKE ALL ON FUNCTION "automation"."fn_dispatch_action"("p_event_id" "uuid", "p_rule_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "automation"."fn_dispatch_action"("p_event_id" "uuid", "p_rule_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "automation"."fn_emit_event"("p_type" "text", "p_source_schema" "text", "p_source_table" "text", "p_source_id" "uuid", "p_payload" "jsonb", "p_source_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "automation"."fn_emit_event"("p_type" "text", "p_source_schema" "text", "p_source_table" "text", "p_source_id" "uuid", "p_payload" "jsonb", "p_source_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "automation"."fn_eval_filter"("p_filter" "jsonb", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "automation"."fn_eval_filter"("p_filter" "jsonb", "p_payload" "jsonb") TO "service_role";
GRANT ALL ON FUNCTION "automation"."fn_eval_filter"("p_filter" "jsonb", "p_payload" "jsonb") TO "authenticated";



REVOKE ALL ON FUNCTION "automation"."fn_jsonpointer_to_jsonpath"("p_pointer" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "automation"."fn_jsonpointer_to_jsonpath"("p_pointer" "text") TO "service_role";
GRANT ALL ON FUNCTION "automation"."fn_jsonpointer_to_jsonpath"("p_pointer" "text") TO "authenticated";



REVOKE ALL ON FUNCTION "automation"."fn_match_rules_for_event"("p_event_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "automation"."fn_match_rules_for_event"("p_event_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "automation"."fn_run_dispatcher"("p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "automation"."fn_run_dispatcher"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "battles"."award_battle_xp"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_advance_tournament"("p_match_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_auto_close_voting"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_auto_close_voting_safe"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_auto_finalize_battles"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_auto_finalize_battles_safe"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_auto_publish_battles"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_auto_start_battles"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_battle_lifecycle_tick"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_battles_publish_internal"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_battles_start_voting_internal"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_check_vote_rate_limit"("p_voter_lenser_id" "uuid", "p_battle_id" "uuid") TO "service_role";
GRANT ALL ON FUNCTION "battles"."fn_check_vote_rate_limit"("p_voter_lenser_id" "uuid", "p_battle_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "battles"."fn_claim_battle_execution_job"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_claim_battle_execution_job"("p_worker_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "battles"."fn_clone_battle_for_rematch"("p_parent_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_clone_battle_for_rematch"("p_parent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_complete_battle_execution_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_create_next_tournament_round"("p_tournament_id" "uuid", "p_round_number" integer) TO "service_role";



REVOKE ALL ON FUNCTION "battles"."fn_create_rematch_internal"("p_parent_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_create_rematch_internal"("p_parent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_dispatch_recurring_battle_templates"() TO "service_role";



REVOKE ALL ON FUNCTION "battles"."fn_dispatch_series_rematches"() FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_dispatch_series_rematches"() TO "service_role";



REVOKE ALL ON FUNCTION "battles"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "service_role";
GRANT ALL ON FUNCTION "battles"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "battles"."fn_get_public_execution_jobs"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_get_public_execution_jobs"("p_battle_id" "uuid") TO "service_role";
GRANT ALL ON FUNCTION "battles"."fn_get_public_execution_jobs"("p_battle_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "battles"."fn_model_test_runs_immutable"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_populate_contender_entity_map"() TO "service_role";



REVOKE ALL ON FUNCTION "battles"."fn_reclaim_stale_battle_jobs"("p_stale_after_minutes" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "battles"."fn_reclaim_stale_battle_jobs"("p_stale_after_minutes" integer) TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_record_ai_judge_verdict"("p_battle_id" "uuid", "p_verdicts" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_refresh_all_vote_velocities"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_refresh_vote_velocity"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_series_has_published_round"("p_series_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_series_round_owner_id"("p_series_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_series_touch_updated"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_submit_agent_vote"("p_battle_id" "uuid", "p_voter_lenser_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_touch_comment_updated_at"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_trg_notify_agent_battle_won"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_trg_notify_battle_comment"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_trg_notify_battle_joined"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_trg_notify_battle_started"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_trg_template_battle_status_notify"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_update_handicap_timestamp"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_validate_comment_lenser"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."fn_votes_set_updated_at"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."log_battle_status_event"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."set_updated_at"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_battle_execution_jobs_capture_agent_snapshot"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_battle_start_requires_challenge"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_battle_structural_immutability"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_contender_entity_map_capture_agent_snapshot"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_enforce_status_transition"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_events_immutable"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_generated_challenge_immutable"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_notify_ai_vote_cast"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_notify_battle_assigned"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_notify_vote_received"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_prevent_self_vote"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_refresh_vote_velocity"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_snapshot_battle_on_first_open"() TO "service_role";



GRANT ALL ON FUNCTION "battles"."trg_submissions_validate_execution_link"() TO "service_role";



REVOKE ALL ON FUNCTION "billing"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "billing"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "service_role";



REVOKE ALL ON FUNCTION "content"."ensure_public_tag"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."ensure_public_tag"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."ensure_public_tag"() TO "service_role";



REVOKE ALL ON FUNCTION "content"."fn_cleanup_entity_refs"("p_entity_type" "content"."entity_type_enum", "p_entity_id" "uuid") FROM PUBLIC;



REVOKE ALL ON FUNCTION "content"."normalize_website_url"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."normalize_website_url"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."normalize_website_url"() TO "service_role";



REVOKE ALL ON FUNCTION "content"."set_updated_at"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."set_updated_at"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."set_updated_at"() TO "service_role";



REVOKE ALL ON FUNCTION "content"."set_version_published_at"() FROM PUBLIC;



REVOKE ALL ON FUNCTION "content"."sync_thread_count"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."sync_thread_count"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."sync_thread_count"() TO "service_role";



REVOKE ALL ON FUNCTION "content"."thread_replies_after_delete_trigger"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."thread_replies_after_delete_trigger"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."thread_replies_after_delete_trigger"() TO "service_role";



REVOKE ALL ON FUNCTION "content"."thread_replies_after_insert_trigger"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."thread_replies_after_insert_trigger"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."thread_replies_after_insert_trigger"() TO "service_role";



REVOKE ALL ON FUNCTION "content"."thread_replies_after_update_trigger"() FROM PUBLIC;
GRANT ALL ON FUNCTION "content"."thread_replies_after_update_trigger"() TO "authenticated";
GRANT ALL ON FUNCTION "content"."thread_replies_after_update_trigger"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."get_auth_lenser_id"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."get_auth_lenser_id"() TO "service_role";



GRANT ALL ON TABLE "content"."thread_replies" TO "service_role";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "content"."thread_replies" TO "authenticated";



GRANT ALL ON FUNCTION "content"."thread_reply_counts_as_public"("r" "content"."thread_replies") TO "anon";
GRANT ALL ON FUNCTION "content"."thread_reply_counts_as_public"("r" "content"."thread_replies") TO "authenticated";
GRANT ALL ON FUNCTION "content"."thread_reply_counts_as_public"("r" "content"."thread_replies") TO "service_role";



GRANT ALL ON FUNCTION "content"."toggle_reaction"("p_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "anon";
GRANT ALL ON FUNCTION "content"."toggle_reaction"("p_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "authenticated";
GRANT ALL ON FUNCTION "content"."toggle_reaction"("p_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "service_role";



GRANT ALL ON FUNCTION "content"."user_owns_thread"("thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "content"."user_owns_thread"("thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "content"."user_owns_thread"("thread_id" "uuid") TO "service_role";
























REVOKE ALL ON FUNCTION "devices"."fn_acquire_leader_lease"("p_lease_kind" "text", "p_device_id" "uuid", "p_lease_seconds" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "devices"."fn_acquire_leader_lease"("p_lease_kind" "text", "p_device_id" "uuid", "p_lease_seconds" integer) TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_approve"("p_device_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_device_revoke"("p_device_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_sync_push"("p_envelope" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_sync_resolve_conflict"("p_outbox_id" "uuid", "p_winner" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "devices"."fn_sync_status"("p_device_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "execution"."fn_async_run_idempotent_complete"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_async_run_idempotent_complete"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_b64url_decode"("p_in" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_b64url_decode"("p_in" "text") TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_b64url_decode"("p_in" "text") TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_claim_queued_run"() FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_claim_queued_run"() TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_complete_async_run"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_complete_async_run"("p_run_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_compute_submission_trust"("p_submission_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_compute_submission_trust"("p_submission_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_compute_submission_trust"("p_submission_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_get_run_details"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_get_run_details"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_get_submission_trust"("p_submission_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_get_submission_trust"("p_submission_id" "uuid") TO "anon";



REVOKE ALL ON FUNCTION "execution"."fn_media_finalize_sync_upload"("p_run_id" "uuid", "p_object_key" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_media_finalize_sync_upload"("p_run_id" "uuid", "p_object_key" "text", "p_mime_type" "text", "p_bytes" bigint, "p_width" integer, "p_height" integer, "p_duration_s" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_persist_local_execution"("p_lens_id" "uuid", "p_version_id" "uuid", "p_provider" "text", "p_model" "text", "p_content_text" "text", "p_token_input" integer, "p_token_output" integer) TO "authenticated";



REVOKE ALL ON FUNCTION "execution"."fn_poll_async_run"("p_stale_after_seconds" integer, "p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_poll_async_run"("p_stale_after_seconds" integer, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_record_execution_attestation"("p_run_id" "uuid", "p_device_id" "uuid", "p_signed" boolean, "p_signature" "text", "p_gateway_verified" boolean, "p_device_trusted" boolean, "p_policy_passed" boolean, "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text") TO "authenticated";



REVOKE ALL ON FUNCTION "execution"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "execution"."fn_run_lens_api"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_run_lens_api"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "execution"."fn_runner_list_with_devices"("p_limit" integer, "p_cursor" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "execution"."fn_start_execution"("p_lenser_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_model_id" "uuid", "p_lens_id" "uuid", "p_workspace_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_start_execution"("p_lenser_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_model_id" "uuid", "p_lens_id" "uuid", "p_workspace_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "execution"."fn_timeout_stale_runs"() TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_verify_attestation_signature"("p_kid" "uuid", "p_canonical_jcs" "bytea", "p_signature" "bytea") FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_verify_attestation_signature"("p_kid" "uuid", "p_canonical_jcs" "bytea", "p_signature" "bytea") TO "authenticated";
GRANT ALL ON FUNCTION "execution"."fn_verify_attestation_signature"("p_kid" "uuid", "p_canonical_jcs" "bytea", "p_signature" "bytea") TO "service_role";



REVOKE ALL ON FUNCTION "execution"."fn_xp_apply_safe"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "execution"."fn_xp_apply_safe"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") TO "service_role";


































































































































































































































































































































































































































































































































































































































































































































































































































































REVOKE ALL ON FUNCTION "internal"."get_service_role_key"() FROM PUBLIC;
GRANT ALL ON FUNCTION "internal"."get_service_role_key"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."anonymize_join_log"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."assign_country_join_order"("p_lenser_id" "uuid", "p_country_code" "text") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."auto_award_badges_from_join_order"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."auto_award_badges_from_level"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_old_level" integer, "p_new_level" integer, "p_event_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."auto_award_badges_from_streak"("p_lenser_id" "uuid", "p_streak" integer, "p_event_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."award_badge"("p_lenser_id" "uuid", "p_type" "lensers"."lenser_badge_type", "p_label" "text", "p_description" "text", "p_icon" "text", "p_xp_event_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."build_author_profile"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."build_author_profile"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."current_active_lenser_id"() TO "anon";
GRANT ALL ON FUNCTION "lensers"."current_active_lenser_id"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."current_active_lenser_id"() TO "service_role";



REVOKE ALL ON FUNCTION "lensers"."enforce_lensers_protections"() FROM PUBLIC;
GRANT ALL ON FUNCTION "lensers"."enforce_lensers_protections"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."enforce_lensers_protections"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."get_auth_human_lenser_id"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."get_auth_human_lenser_id"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."init_lenser_engagement_row"() TO "anon";
GRANT ALL ON FUNCTION "lensers"."init_lenser_engagement_row"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."init_lenser_engagement_row"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."is_active_lenser"("p_user_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "lensers"."is_active_lenser"("p_user_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."is_active_lenser"("p_user_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "lensers"."log_account_lifecycle_event"("p_profile_id" "uuid", "p_user_id" "uuid", "p_event_type" "text", "p_from_status" "lensers"."lenser_status", "p_to_status" "lensers"."lenser_status", "p_actor_source" "text", "p_metadata" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "lensers"."log_account_lifecycle_event"("p_profile_id" "uuid", "p_user_id" "uuid", "p_event_type" "text", "p_from_status" "lensers"."lenser_status", "p_to_status" "lensers"."lenser_status", "p_actor_source" "text", "p_metadata" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "lensers"."log_lenser_join"() TO "anon";
GRANT ALL ON FUNCTION "lensers"."log_lenser_join"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."log_lenser_join"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."prevent_lenser_join_log_delete"() TO "anon";
GRANT ALL ON FUNCTION "lensers"."prevent_lenser_join_log_delete"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."prevent_lenser_join_log_delete"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."prevent_lenser_join_log_update"() TO "anon";
GRANT ALL ON FUNCTION "lensers"."prevent_lenser_join_log_update"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."prevent_lenser_join_log_update"() TO "service_role";



REVOKE ALL ON FUNCTION "lensers"."protect_sensitive_lenser_fields"() FROM PUBLIC;
GRANT ALL ON FUNCTION "lensers"."protect_sensitive_lenser_fields"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."protect_sensitive_lenser_fields"() TO "service_role";



REVOKE ALL ON FUNCTION "lensers"."sync_join_order"() FROM PUBLIC;
GRANT ALL ON FUNCTION "lensers"."sync_join_order"() TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."sync_join_order"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."trg_award_founder_badges"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."trg_create_join_log"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."trg_handle_deletion_request"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."trg_log_lenser_activity"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."trg_log_login_from_auth"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."trg_update_lenser_last_login"() TO "service_role";



GRANT ALL ON FUNCTION "lensers"."user_owns_lenser"("lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "lensers"."user_owns_lenser"("lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "lensers"."user_owns_lenser"("lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "lenses"."fn_build_schedule_condition_context"("p_schedule_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "lenses"."fn_build_schedule_condition_context"("p_schedule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "lenses"."fn_build_schedule_condition_context"("p_schedule_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "lenses"."fn_check_calendar"("p_calendar_id" "uuid", "p_now" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "lenses"."fn_check_calendar"("p_calendar_id" "uuid", "p_now" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "lenses"."fn_check_calendar"("p_calendar_id" "uuid", "p_now" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "lenses"."fn_claim_scheduled_workflow_run"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "lenses"."fn_claim_scheduled_workflow_run"("p_worker_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "lenses"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "authenticated";



GRANT ALL ON TABLE "lenses"."versions" TO "authenticated";
GRANT ALL ON TABLE "lenses"."versions" TO "service_role";



GRANT ALL ON FUNCTION "lenses"."fn_create_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_create_lens"("p_visibility" "content"."visibility_enum", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_dispatch_scheduled_workflows"() TO "service_role";



GRANT ALL ON FUNCTION "lenses"."fn_get_version_params_with_tools"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "lenses"."fn_get_version_params_with_tools"("p_version_id" "uuid") TO "anon";



GRANT ALL ON FUNCTION "lenses"."fn_list_tools"("p_category" "text") TO "authenticated";
GRANT ALL ON FUNCTION "lenses"."fn_list_tools"("p_category" "text") TO "anon";



GRANT ALL ON FUNCTION "lenses"."fn_list_versions"("p_lens_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "lenses"."fn_preview_schedule_ticks"("p_schedule_id" "uuid", "p_n" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "lenses"."fn_preview_schedule_ticks"("p_schedule_id" "uuid", "p_n" integer) TO "authenticated";
GRANT ALL ON FUNCTION "lenses"."fn_preview_schedule_ticks"("p_schedule_id" "uuid", "p_n" integer) TO "service_role";



GRANT ALL ON FUNCTION "lenses"."fn_publish_version"("p_version_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_render_version_body"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "lenses"."fn_render_version_body"("p_version_id" "uuid") TO "anon";



GRANT ALL ON FUNCTION "lenses"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text", "p_visibility" "content"."visibility_enum", "p_title" "text", "p_description" "text", "p_tag_ids" "uuid"[]) TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_update_lens"("p_lens_id" "uuid", "p_template_body" "text", "p_visibility" "content"."visibility_enum", "p_title" "text", "p_description" "text", "p_tag_ids" "uuid"[], "p_params" "jsonb") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_upsert_draft_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "authenticated";



GRANT ALL ON FUNCTION "lenses"."fn_upsert_workflow_schedule_internal"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "media"."set_updated_at"() FROM PUBLIC;
GRANT ALL ON FUNCTION "media"."set_updated_at"() TO "authenticated";
GRANT ALL ON FUNCTION "media"."set_updated_at"() TO "service_role";









GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."organizations" TO "anon";
GRANT ALL ON TABLE "organizations"."organizations" TO "authenticated";
GRANT ALL ON TABLE "organizations"."organizations" TO "service_role";



GRANT ALL ON FUNCTION "organizations"."fn_create_organization"("p_display_name" character varying, "p_slug" character varying, "p_org_type" "text", "p_legal_name" character varying, "p_website" "text", "p_industry" character varying, "p_billing_email" "text") TO "authenticated";
GRANT ALL ON FUNCTION "organizations"."fn_create_organization"("p_display_name" character varying, "p_slug" character varying, "p_org_type" "text", "p_legal_name" character varying, "p_website" "text", "p_industry" character varying, "p_billing_email" "text") TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."members" TO "anon";
GRANT ALL ON TABLE "organizations"."members" TO "authenticated";
GRANT ALL ON TABLE "organizations"."members" TO "service_role";



GRANT ALL ON FUNCTION "organizations"."fn_invite_member"("p_org_id" "uuid", "p_lenser_id" "uuid", "p_role" "text") TO "authenticated";
GRANT ALL ON FUNCTION "organizations"."fn_invite_member"("p_org_id" "uuid", "p_lenser_id" "uuid", "p_role" "text") TO "service_role";



REVOKE ALL ON FUNCTION "platform"."fn_get_platform_system_flags"() FROM PUBLIC;
GRANT ALL ON FUNCTION "platform"."fn_get_platform_system_flags"() TO "service_role";



REVOKE ALL ON FUNCTION "platform"."fn_queue_stats"() FROM PUBLIC;
GRANT ALL ON FUNCTION "platform"."fn_queue_stats"() TO "service_role";



REVOKE ALL ON FUNCTION "platform"."fn_recent_audit_events"("p_since" interval) FROM PUBLIC;
GRANT ALL ON FUNCTION "platform"."fn_recent_audit_events"("p_since" interval) TO "service_role";



REVOKE ALL ON FUNCTION "platform"."fn_set_autonomy_dispatch_enabled"("p_enabled" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "platform"."fn_set_autonomy_dispatch_enabled"("p_enabled" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "platform"."fn_set_platform_flag"("p_key" "text", "p_enabled" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "platform"."fn_set_platform_flag"("p_key" "text", "p_enabled" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "service_role";
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "supabase_auth_admin";



REVOKE ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "audit"."vote_anomalies" TO "service_role";
GRANT SELECT ON TABLE "audit"."vote_anomalies" TO "authenticated";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_worker_health"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_worker_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_worker_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_worker_health"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_health"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_health"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."series" TO "authenticated";
GRANT ALL ON TABLE "battles"."series" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_catalog_providers"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_providers"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_providers"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") TO "service_role";



GRANT ALL ON TABLE "analytics"."shared_links" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tool_assignments" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tool_assignments" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."templates" TO "authenticated";
GRANT ALL ON TABLE "battles"."templates" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "battles"."submissions" TO "authenticated";
GRANT ALL ON TABLE "battles"."submissions" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "execution"."byok_keys" TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_rotation_due"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_rotation_due"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_rotation_due"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_rotation_due"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_account_deletion_on_login"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_account_deletion_on_login"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_account_deletion_on_login"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_handle"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_handle"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_handle"("p_handle" "text") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."media_quality_results" TO "authenticated";
GRANT ALL ON TABLE "battles"."media_quality_results" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."scratchpad_runs" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."scratchpad_runs" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connectors_list"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connectors_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connectors_list"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_core_languages_list"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_core_languages_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_core_languages_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_core_languages_list"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."tournaments" TO "authenticated";
GRANT ALL ON TABLE "battles"."tournaments" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_deactivate_account"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_deactivate_account"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_deactivate_account"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_deny_mutation"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_deny_mutation"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_deny_mutation"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_expire_byok_keys"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_expire_byok_keys"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_expire_media_objects"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_expire_media_objects"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_expire_media_objects"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_expire_media_objects"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_expire_stale_approvals"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_expire_stale_approvals"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_expire_stale_approvals"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_auth_profile_gate"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_auth_profile_gate"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_auth_profile_gate"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "battles"."battles" TO "authenticated";
GRANT ALL ON TABLE "battles"."battles" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."contender_entity_map" TO "authenticated";
GRANT ALL ON TABLE "battles"."contender_entity_map" TO "service_role";



GRANT SELECT,INSERT ON TABLE "battles"."contenders" TO "authenticated";
GRANT ALL ON TABLE "battles"."contenders" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."rule_snapshots" TO "authenticated";
GRANT ALL ON TABLE "battles"."rule_snapshots" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."vote_aggregates" TO "authenticated";
GRANT ALL ON TABLE "battles"."vote_aggregates" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."v_battle_full" TO "authenticated";
GRANT ALL ON TABLE "battles"."v_battle_full" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_dlq_counts"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_dlq_counts"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_counts"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_counts"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_execution_status"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_execution_status"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_execution_status"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_gateway_device_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_gateway_device_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_gateway_device_health"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON TABLE "lensers"."profiles" TO "service_role";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lensers"."profiles" TO "authenticated";



GRANT SELECT ON TABLE "reputation"."contender_ratings" TO "authenticated";
GRANT SELECT ON TABLE "reputation"."contender_ratings" TO "service_role";



GRANT SELECT ON TABLE "reputation"."lenser_scores" TO "authenticated";
GRANT SELECT ON TABLE "reputation"."lenser_scores" TO "service_role";



GRANT ALL ON TABLE "xp"."totals" TO "service_role";
GRANT SELECT ON TABLE "xp"."totals" TO "authenticated";



GRANT ALL ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_language_preference"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_language_preference"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_language_preference"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON TABLE "analytics"."lenser_stats" TO "service_role";
GRANT SELECT ON TABLE "analytics"."lenser_stats" TO "authenticated";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."model_test_runs" TO "authenticated";
GRANT ALL ON TABLE "battles"."model_test_runs" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_my_api_keys"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_my_api_keys"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_api_keys"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_api_keys"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_lensers"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_lensers"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_lensers"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_notification_preferences"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_notification_preferences"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_notification_preferences"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."provider_configs" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."provider_configs" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_schedule_calendars"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_calendars"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_calendars"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_series"("p_series_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_series"("p_series_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_series"("p_series_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_unread_notification_count"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_unread_notification_count"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_unread_notification_count"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_health"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."agent_run_events" TO "authenticated";



GRANT SELECT ON TABLE "agents"."ai_lensers" TO "authenticated";
GRANT ALL ON TABLE "agents"."ai_lensers" TO "service_role";



GRANT SELECT ON TABLE "agents"."ownerships" TO "authenticated";
GRANT ALL ON TABLE "agents"."ownerships" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."team_runs" TO "authenticated";



GRANT SELECT ON TABLE "agents"."v_human_fleet_logs" TO "authenticated";
GRANT SELECT ON TABLE "agents"."v_human_fleet_logs" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT SELECT ON TABLE "agents"."v_human_fleet_runs" TO "authenticated";
GRANT SELECT ON TABLE "agents"."v_human_fleet_runs" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_is_super_admin"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_is_super_admin"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_is_super_admin"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_is_super_admin"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_journey_state_get"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_journey_state_get"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_journey_state_get"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_journey_state_get"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_list"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_active_profile"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_active_profile"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_active_profile"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_current_id"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_current_id"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_current_id"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_fresh_profile"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_fresh_profile"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_fresh_profile"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_get_preferences"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_get_preferences"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_preferences"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_preferences"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_request_deletion"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_request_deletion"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_request_deletion"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_request_deletion"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_starred"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_starred"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_starred"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lifecycle_is_service_role"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lifecycle_is_service_role"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lifecycle_is_service_role"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";
GRANT ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";



GRANT ALL ON FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";
GRANT ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";



GRANT ALL ON FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tools"("p_category" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tools"("p_category" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tools"("p_category" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_list_connections"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_list_connections"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_list_connections"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."memory_profiles" TO "authenticated";



GRANT ALL ON FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_purge_due_accounts"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_purge_due_accounts"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_purge_due_accounts"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_queue_unfreeze"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_queue_unfreeze"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_queue_unfreeze"() TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."memories" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."memories" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tools_registry" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tools_registry" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."tournament_contenders" TO "authenticated";
GRANT ALL ON TABLE "battles"."tournament_contenders" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."workspace_settings" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."workspace_settings" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_list"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_schedule_account_deletion"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_schedule_account_deletion"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_schedule_account_deletion"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_timeout_stale_runs_safe"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs_safe"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_trg_battles_status_webhook"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_trg_battles_status_webhook"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_trg_battles_status_webhook"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_trg_votes_velocity_check"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_trg_votes_velocity_check"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_trg_votes_velocity_check"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "service_role";



GRANT SELECT ON TABLE "agents"."lens_bindings" TO "authenticated";
GRANT ALL ON TABLE "agents"."lens_bindings" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) TO "service_role";



GRANT SELECT ON TABLE "agents"."model_bindings" TO "authenticated";
GRANT ALL ON TABLE "agents"."model_bindings" TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_team_run"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_team_run"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_team_run"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_team_run"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_apps"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_apps"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_apps"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_self"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_self"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_self"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."get_active_providers"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."get_active_providers"() TO "anon";
GRANT ALL ON FUNCTION "public"."get_active_providers"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_active_providers"() TO "service_role";



GRANT ALL ON FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."guard_notification_fields"() TO "anon";
GRANT ALL ON FUNCTION "public"."guard_notification_fields"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."guard_notification_fields"() TO "service_role";



GRANT ALL ON FUNCTION "public"."is_admin"() TO "anon";
GRANT ALL ON FUNCTION "public"."is_admin"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."is_admin"() TO "service_role";



GRANT ALL ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."set_updated_at"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "service_role";



GRANT ALL ON FUNCTION "public"."trg_block_unsafe_artifact_delete"() TO "anon";
GRANT ALL ON FUNCTION "public"."trg_block_unsafe_artifact_delete"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."trg_block_unsafe_artifact_delete"() TO "service_role";



REVOKE ALL ON FUNCTION "status"."fn_status_incident_detail"("p_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "status"."fn_status_incident_detail"("p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "status"."fn_status_incident_detail"("p_id" "uuid") TO "authenticated";



REVOKE ALL ON FUNCTION "status"."fn_status_service_detail"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "status"."fn_status_service_detail"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "status"."fn_status_service_detail"("p_slug" "text") TO "authenticated";



REVOKE ALL ON FUNCTION "status"."fn_status_snapshot"("p_snapshot_date" "date", "p_scope_type" "text", "p_scope_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "status"."fn_status_snapshot"("p_snapshot_date" "date", "p_scope_type" "text", "p_scope_key" "text") TO "anon";
GRANT ALL ON FUNCTION "status"."fn_status_snapshot"("p_snapshot_date" "date", "p_scope_type" "text", "p_scope_key" "text") TO "authenticated";



REVOKE ALL ON FUNCTION "status"."fn_status_summary"() FROM PUBLIC;
GRANT ALL ON FUNCTION "status"."fn_status_summary"() TO "anon";
GRANT ALL ON FUNCTION "status"."fn_status_summary"() TO "authenticated";



REVOKE ALL ON FUNCTION "status"."fn_status_uptime_rollup"("p_window" "status"."status_window", "p_scope_type" "text", "p_scope_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "status"."fn_status_uptime_rollup"("p_window" "status"."status_window", "p_scope_type" "text", "p_scope_key" "text") TO "anon";
GRANT ALL ON FUNCTION "status"."fn_status_uptime_rollup"("p_window" "status"."status_window", "p_scope_type" "text", "p_scope_key" "text") TO "authenticated";



GRANT ALL ON FUNCTION "tenancy"."fn_create_personal_workspace"() TO "anon";
GRANT ALL ON FUNCTION "tenancy"."fn_create_personal_workspace"() TO "authenticated";
GRANT ALL ON FUNCTION "tenancy"."fn_create_personal_workspace"() TO "service_role";



GRANT ALL ON FUNCTION "tenancy"."is_workspace_admin"("p_workspace_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "tenancy"."is_workspace_admin"("p_workspace_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "tenancy"."is_workspace_admin"("p_workspace_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "tenancy"."is_workspace_member"("p_workspace_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "tenancy"."is_workspace_member"("p_workspace_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "tenancy"."is_workspace_member"("p_workspace_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "tenancy"."set_updated_at"() FROM PUBLIC;
GRANT ALL ON FUNCTION "tenancy"."set_updated_at"() TO "authenticated";
GRANT ALL ON FUNCTION "tenancy"."set_updated_at"() TO "service_role";












REVOKE ALL ON FUNCTION "wallet"."credit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."credit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."debit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."debit_account"("p_lenser_id" "uuid", "p_amount" bigint, "p_tx_type" "wallet"."transaction_type_enum", "p_reference_type" "text", "p_reference_id" "uuid", "p_description" "text", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."get_balance"("p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."get_balance"("p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."get_wallet_summary"("p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."get_wallet_summary"("p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid", "p_expected_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."release_reservation"("p_reservation_id" "uuid", "p_expected_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."reserve_credits"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "uuid", "p_description" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."reserve_credits"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "uuid", "p_description" "text") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."reserve_credits_safe"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "text", "p_description" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."reserve_credits_safe"("p_lenser_id" "uuid", "p_amount" bigint, "p_reference_id" "text", "p_description" "text") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."reset_spending_period"("p_period" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."reset_spending_period"("p_period" "text") TO "service_role";



REVOKE ALL ON FUNCTION "wallet"."settle_charge"("p_reservation_id" "uuid", "p_actual_amount" bigint) FROM PUBLIC;
GRANT ALL ON FUNCTION "wallet"."settle_charge"("p_reservation_id" "uuid", "p_actual_amount" bigint) TO "service_role";



REVOKE ALL ON FUNCTION "xp"."apply"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."apply"("p_lenser_id" "uuid", "p_rule_key" "text", "p_source" "xp"."source_enum", "p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_app_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "xp"."auto_create_next_season"("p_app_id" "uuid", "p_duration_days" integer) TO "service_role";



GRANT ALL ON FUNCTION "xp"."check_all_seasons"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."check_and_rollover_season"("p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."check_and_rollover_season"("p_app_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "xp"."compute_level_from"("p_app_id" "uuid", "p_total_xp" bigint) FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."compute_level_from"("p_app_id" "uuid", "p_total_xp" bigint) TO "service_role";



GRANT ALL ON FUNCTION "xp"."fn_refresh_monthly_rollup"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."fn_trigger_badge_check"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."fn_xp_auto_activate_seasons"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."fn_xp_auto_activate_seasons_safe"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."generate_levels"("p_app_id" "uuid", "p_max_level" integer, "p_base_xp" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."generate_levels"("p_app_id" "uuid", "p_max_level" integer, "p_base_xp" integer) TO "service_role";



REVOKE ALL ON FUNCTION "xp"."get_active_season_id"("p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."get_active_season_id"("p_app_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "xp"."grant_contribution_xp"("p_lenser_id" "uuid", "p_context" "xp"."contribution_context_enum", "p_contribution_type" "text", "p_external_ref" "text", "p_title" "text", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "xp"."mark_event_verified"("p_event_id" "uuid", "p_ai_verified" boolean, "p_verifier" "text", "p_details" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."mark_event_verified"("p_event_id" "uuid", "p_ai_verified" boolean, "p_verifier" "text", "p_details" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "xp"."on_battle_created"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_lens_created"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_lens_created"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."on_lens_visibility_changed"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_reaction_given"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_reaction_given"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_reaction_received"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_reaction_received"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_reply_created"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_reply_created"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_reply_received"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_reply_received"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_thread_created"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_thread_created"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."on_thread_engaged"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."on_thread_engaged"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."on_thread_visibility_changed"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."on_workflow_created"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."on_workflow_visibility_changed"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."prevent_event_mutations"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."prevent_event_mutations"() TO "service_role";



REVOKE ALL ON FUNCTION "xp"."rollback_content_xp"("p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."rollback_content_xp"("p_source_ref_type" "text", "p_source_ref_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "xp"."rollback_event"("p_event_id" "uuid", "p_reason" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."rollback_event"("p_event_id" "uuid", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "xp"."rollover_season"("p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."rollover_season"("p_app_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "xp"."seed_default_curve"("p_app_id" "uuid", "p_max_level" integer, "p_base" numeric, "p_power" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."seed_default_curve"("p_app_id" "uuid", "p_max_level" integer, "p_base" numeric, "p_power" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "xp"."sync_total"() FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."sync_total"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."trg_ai_lenser_battle_xp_fn"() TO "service_role";



GRANT ALL ON FUNCTION "xp"."update_daily_streak"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_streak_type" "text", "p_event_date" "date") TO "anon";
GRANT ALL ON FUNCTION "xp"."update_daily_streak"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_streak_type" "text", "p_event_date" "date") TO "authenticated";
GRANT ALL ON FUNCTION "xp"."update_daily_streak"("p_lenser_id" "uuid", "p_app_id" "uuid", "p_streak_type" "text", "p_event_date" "date") TO "service_role";



REVOKE ALL ON FUNCTION "xp"."update_season_totals"("p_app_id" "uuid", "p_lenser_id" "uuid", "p_xp" bigint) FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."update_season_totals"("p_app_id" "uuid", "p_lenser_id" "uuid", "p_xp" bigint) TO "service_role";



REVOKE ALL ON FUNCTION "xp"."verify_signature"("p_signature" "text", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "xp"."verify_signature"("p_signature" "text", "p_payload" "jsonb") TO "service_role";



GRANT SELECT ON TABLE "admin"."execution_control" TO "service_role";



GRANT SELECT ON TABLE "admin"."kill_switches" TO "service_role";



GRANT SELECT ON TABLE "agents"."action_logs" TO "authenticated";
GRANT ALL ON TABLE "agents"."action_logs" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."agent_run_steps" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."workflow_assignments" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."workflows" TO "authenticated";



GRANT SELECT ON TABLE "agents"."approval_requests_v" TO "authenticated";
GRANT SELECT ON TABLE "agents"."approval_requests_v" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_baselines" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_baselines" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_case_results" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_case_results" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_cases" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_cases" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_runs" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_runs" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluations" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluations" TO "service_role";



GRANT SELECT ON TABLE "agents"."evaluation_results_v" TO "authenticated";
GRANT SELECT ON TABLE "agents"."evaluation_results_v" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_rubrics" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."evaluation_rubrics" TO "service_role";



GRANT SELECT ON TABLE "agents"."gateway_devices" TO "authenticated";



GRANT SELECT ON TABLE "agents"."memories_v" TO "authenticated";
GRANT SELECT ON TABLE "agents"."memories_v" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."memory_access_logs" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."memory_access_logs" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."model_profiles" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."personality_profiles" TO "authenticated";



GRANT SELECT,UPDATE ON TABLE "agents"."policies" TO "authenticated";
GRANT ALL ON TABLE "agents"."policies" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."policy_evaluations" TO "service_role";
GRANT SELECT,INSERT ON TABLE "agents"."policy_evaluations" TO "authenticated";



GRANT SELECT ON TABLE "agents"."quota_snapshots" TO "authenticated";
GRANT ALL ON TABLE "agents"."quota_snapshots" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."run_incidents" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."run_incidents" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."run_reports" TO "service_role";
GRANT SELECT,INSERT ON TABLE "agents"."run_reports" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."standing_approvals" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."team_edges" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."team_members" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."teams" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tool_invocations" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tool_invocations" TO "service_role";



GRANT SELECT ON TABLE "agents"."tool_invocations_v" TO "authenticated";
GRANT SELECT ON TABLE "agents"."tool_invocations_v" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "agents"."tool_profiles" TO "authenticated";



GRANT SELECT ON TABLE "agents"."v_agent_profile" TO "authenticated";
GRANT SELECT ON TABLE "agents"."v_agent_profile" TO "service_role";



GRANT SELECT ON TABLE "agents"."v_human_fleet_overview" TO "authenticated";
GRANT SELECT ON TABLE "agents"."v_human_fleet_overview" TO "service_role";



GRANT SELECT,INSERT ON TABLE "lenses"."workflow_runs" TO "authenticated";



GRANT SELECT ON TABLE "agents"."v_run_unified" TO "authenticated";
GRANT SELECT ON TABLE "agents"."v_run_unified" TO "service_role";



GRANT SELECT ON TABLE "agents"."v_team_run_conversation" TO "authenticated";
GRANT SELECT ON TABLE "agents"."v_team_run_conversation" TO "service_role";



GRANT SELECT,INSERT ON TABLE "agents"."workspace_switches" TO "authenticated";



GRANT SELECT ON TABLE "ai"."key_usage_log" TO "authenticated";
GRANT ALL ON TABLE "ai"."key_usage_log" TO "service_role";



GRANT ALL ON TABLE "ai"."keys" TO "service_role";



GRANT SELECT ON TABLE "ai"."model_pricing" TO "authenticated";
GRANT ALL ON TABLE "ai"."model_pricing" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "ai"."models" TO "anon";
GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "ai"."models" TO "authenticated";
GRANT ALL ON TABLE "ai"."models" TO "service_role";



GRANT ALL ON TABLE "ai"."providers" TO "service_role";



GRANT SELECT ON TABLE "analytics"."agent_cost_daily" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "analytics"."agent_cost_daily" TO "service_role";



GRANT SELECT ON TABLE "analytics"."eval_quality_daily" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "analytics"."eval_quality_daily" TO "service_role";



GRANT ALL ON TABLE "analytics"."lenser_activity" TO "service_role";



GRANT ALL ON TABLE "analytics"."lenser_join_log" TO "service_role";
GRANT SELECT ON TABLE "analytics"."lenser_join_log" TO "authenticated";



GRANT ALL ON SEQUENCE "analytics"."lenser_join_sequence" TO "anon";
GRANT ALL ON SEQUENCE "analytics"."lenser_join_sequence" TO "authenticated";
GRANT ALL ON SEQUENCE "analytics"."lenser_join_sequence" TO "service_role";



GRANT ALL ON TABLE "analytics"."page_views" TO "service_role";



GRANT ALL ON TABLE "analytics"."product_feedback" TO "service_role";
GRANT INSERT ON TABLE "analytics"."product_feedback" TO "anon";
GRANT SELECT,INSERT ON TABLE "analytics"."product_feedback" TO "authenticated";



GRANT ALL ON TABLE "analytics"."share_events" TO "service_role";



GRANT ALL ON TABLE "analytics"."tag_activity_events" TO "service_role";
GRANT SELECT ON TABLE "analytics"."tag_activity_events" TO "authenticated";



GRANT ALL ON SEQUENCE "analytics"."tag_activity_events_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "analytics"."tag_activity_events_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "analytics"."tag_activity_events_id_seq" TO "service_role";



GRANT SELECT ON TABLE "analytics"."workflow_perf_daily" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "analytics"."workflow_perf_daily" TO "service_role";



GRANT SELECT ON TABLE "audit"."account_lifecycle" TO "service_role";



GRANT ALL ON TABLE "audit"."admin_actions" TO "service_role";



GRANT ALL ON TABLE "audit"."events" TO "service_role";
GRANT SELECT,INSERT ON TABLE "audit"."events" TO "authenticated";



GRANT ALL ON TABLE "audit"."moderation_decisions" TO "service_role";



GRANT ALL ON TABLE "audit"."security_events" TO "service_role";
GRANT SELECT ON TABLE "audit"."security_events" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "audit"."webhook_outbox" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "automation"."cron_runs" TO "service_role";



GRANT SELECT ON TABLE "automation"."event_dispatches" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "automation"."event_dispatches" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "automation"."trigger_rules" TO "authenticated";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "automation"."trigger_rules" TO "service_role";



GRANT ALL ON TABLE "battles"."ai_handicap_policies" TO "service_role";
GRANT SELECT ON TABLE "battles"."ai_handicap_policies" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."ai_judge_verdicts" TO "authenticated";
GRANT ALL ON TABLE "battles"."ai_judge_verdicts" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."battle_event_subscriptions" TO "authenticated";
GRANT ALL ON TABLE "battles"."battle_event_subscriptions" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."battle_execution_dead_letters" TO "authenticated";
GRANT ALL ON TABLE "battles"."battle_execution_dead_letters" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."battle_execution_jobs" TO "authenticated";
GRANT ALL ON TABLE "battles"."battle_execution_jobs" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."comments" TO "authenticated";
GRANT ALL ON TABLE "battles"."comments" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."contender_lens_assignments" TO "authenticated";
GRANT ALL ON TABLE "battles"."contender_lens_assignments" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "battles"."contender_runs" TO "anon";
GRANT ALL ON TABLE "battles"."contender_runs" TO "authenticated";
GRANT ALL ON TABLE "battles"."contender_runs" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "battles"."cost_ledgers" TO "anon";
GRANT ALL ON TABLE "battles"."cost_ledgers" TO "authenticated";
GRANT ALL ON TABLE "battles"."cost_ledgers" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."events" TO "authenticated";
GRANT ALL ON TABLE "battles"."events" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."execution_configs" TO "authenticated";
GRANT ALL ON TABLE "battles"."execution_configs" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "battles"."funding_policies" TO "anon";
GRANT ALL ON TABLE "battles"."funding_policies" TO "authenticated";
GRANT ALL ON TABLE "battles"."funding_policies" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."generated_challenges" TO "authenticated";
GRANT ALL ON TABLE "battles"."generated_challenges" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."global_messages" TO "authenticated";
GRANT ALL ON TABLE "battles"."global_messages" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."invitations" TO "authenticated";
GRANT ALL ON TABLE "battles"."invitations" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."invite_links" TO "authenticated";
GRANT ALL ON TABLE "battles"."invite_links" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."media_quality_rules" TO "authenticated";
GRANT ALL ON TABLE "battles"."media_quality_rules" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "battles"."rubric_criteria" TO "authenticated";
GRANT ALL ON TABLE "battles"."rubric_criteria" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "battles"."rubrics" TO "authenticated";
GRANT ALL ON TABLE "battles"."rubrics" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."schedules" TO "authenticated";
GRANT ALL ON TABLE "battles"."schedules" TO "service_role";



GRANT SELECT ON TABLE "battles"."scorecards" TO "authenticated";
GRANT ALL ON TABLE "battles"."scorecards" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."scoring_plugin_signals" TO "authenticated";
GRANT ALL ON TABLE "battles"."scoring_plugin_signals" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."series_rounds" TO "authenticated";
GRANT ALL ON TABLE "battles"."series_rounds" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."sponsorship_contributions" TO "authenticated";
GRANT ALL ON TABLE "battles"."sponsorship_contributions" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."sponsorship_payouts" TO "authenticated";
GRANT ALL ON TABLE "battles"."sponsorship_payouts" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."sponsorship_pools" TO "authenticated";
GRANT ALL ON TABLE "battles"."sponsorship_pools" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."stream_recordings" TO "authenticated";
GRANT ALL ON TABLE "battles"."stream_recordings" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."template_prompt_variables" TO "authenticated";
GRANT ALL ON TABLE "battles"."template_prompt_variables" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."tool_registry" TO "authenticated";
GRANT ALL ON TABLE "battles"."tool_registry" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."tournament_matches" TO "authenticated";
GRANT ALL ON TABLE "battles"."tournament_matches" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."tournament_rounds" TO "authenticated";
GRANT ALL ON TABLE "battles"."tournament_rounds" TO "service_role";



GRANT SELECT ON TABLE "battles"."v_battle_feed_item" TO "authenticated";
GRANT ALL ON TABLE "battles"."v_battle_feed_item" TO "service_role";



GRANT ALL ON TABLE "execution"."runs" TO "service_role";
GRANT SELECT ON TABLE "execution"."runs" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."v_benchmark_dataset" TO "authenticated";
GRANT ALL ON TABLE "battles"."v_benchmark_dataset" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."v_execution_jobs_public" TO "authenticated";
GRANT ALL ON TABLE "battles"."v_execution_jobs_public" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "battles"."vote_choices" TO "authenticated";
GRANT ALL ON TABLE "battles"."vote_choices" TO "service_role";



GRANT SELECT,INSERT,DELETE ON TABLE "battles"."votes" TO "authenticated";
GRANT ALL ON TABLE "battles"."votes" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "billing"."budget_policies" TO "service_role";



GRANT ALL ON TABLE "billing"."checkout_sessions" TO "service_role";



GRANT SELECT ON TABLE "billing"."cost_reservations" TO "authenticated";
GRANT SELECT,INSERT,UPDATE ON TABLE "billing"."cost_reservations" TO "service_role";



GRANT ALL ON TABLE "billing"."execution_margin_policies" TO "service_role";



GRANT SELECT ON TABLE "billing"."ledger_entries" TO "authenticated";
GRANT SELECT,INSERT ON TABLE "billing"."ledger_entries" TO "service_role";



GRANT USAGE ON SEQUENCE "billing"."ledger_entries_id_seq" TO "service_role";



GRANT ALL ON TABLE "billing"."orders" TO "service_role";



GRANT SELECT,INSERT ON TABLE "billing"."pricing_snapshots" TO "service_role";



GRANT ALL ON TABLE "billing"."products" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "billing"."provider_circuit_state" TO "service_role";



GRANT SELECT ON TABLE "billing"."runtime_settings" TO "authenticated";
GRANT SELECT,INSERT,UPDATE ON TABLE "billing"."runtime_settings" TO "service_role";



GRANT SELECT,INSERT ON TABLE "billing"."spend_anomalies" TO "service_role";



GRANT ALL ON TABLE "billing"."variants" TO "service_role";



GRANT SELECT ON TABLE "billing"."vw_products" TO "service_role";
GRANT SELECT ON TABLE "billing"."vw_products" TO "authenticated";



GRANT ALL ON TABLE "billing"."webhook_events" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "connectors"."connector_tokens" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "connectors"."connectors" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "content"."entity_translations" TO "anon";
GRANT ALL ON TABLE "content"."entity_translations" TO "authenticated";
GRANT ALL ON TABLE "content"."entity_translations" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "content"."reactions" TO "anon";
GRANT ALL ON TABLE "content"."reactions" TO "authenticated";
GRANT ALL ON TABLE "content"."reactions" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "content"."tag_map" TO "authenticated";



GRANT ALL ON TABLE "content"."tag_suggestions" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "content"."tag_translations" TO "anon";
GRANT ALL ON TABLE "content"."tag_translations" TO "authenticated";
GRANT ALL ON TABLE "content"."tag_translations" TO "service_role";



GRANT ALL ON TABLE "content"."tags" TO "service_role";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "content"."tags" TO "authenticated";



GRANT ALL ON TABLE "content"."threads" TO "service_role";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "content"."threads" TO "authenticated";



GRANT ALL ON TABLE "lenses"."lenses" TO "service_role";
GRANT ALL ON TABLE "lenses"."lenses" TO "authenticated";



GRANT SELECT ON TABLE "content"."vw_auth_threads" TO "authenticated";
GRANT SELECT ON TABLE "content"."vw_auth_threads" TO "service_role";



GRANT SELECT ON TABLE "content"."vw_tag_cross_lang" TO "authenticated";
GRANT SELECT ON TABLE "content"."vw_tag_cross_lang" TO "service_role";



GRANT ALL ON TABLE "core"."cities" TO "service_role";
GRANT SELECT ON TABLE "core"."cities" TO "authenticated";



GRANT ALL ON SEQUENCE "core"."cities_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "core"."cities_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "core"."cities_id_seq" TO "service_role";



GRANT ALL ON TABLE "core"."countries" TO "service_role";
GRANT SELECT ON TABLE "core"."countries" TO "authenticated";



GRANT ALL ON SEQUENCE "core"."countries_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "core"."countries_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "core"."countries_id_seq" TO "service_role";



GRANT ALL ON TABLE "core"."currencies" TO "service_role";
GRANT SELECT ON TABLE "core"."currencies" TO "authenticated";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "core"."languages" TO "anon";
GRANT ALL ON TABLE "core"."languages" TO "authenticated";
GRANT ALL ON TABLE "core"."languages" TO "service_role";



GRANT ALL ON TABLE "core"."regions" TO "service_role";
GRANT SELECT ON TABLE "core"."regions" TO "authenticated";



GRANT ALL ON SEQUENCE "core"."regions_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "core"."regions_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "core"."regions_id_seq" TO "service_role";



GRANT ALL ON TABLE "core"."states" TO "service_role";
GRANT SELECT ON TABLE "core"."states" TO "authenticated";



GRANT ALL ON SEQUENCE "core"."states_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "core"."states_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "core"."states_id_seq" TO "service_role";



GRANT ALL ON TABLE "core"."subregions" TO "service_role";
GRANT SELECT ON TABLE "core"."subregions" TO "authenticated";



GRANT ALL ON SEQUENCE "core"."subregions_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "core"."subregions_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "core"."subregions_id_seq" TO "service_role";









GRANT ALL ON TABLE "execution"."artifact_medias" TO "service_role";



GRANT ALL ON TABLE "execution"."artifacts" TO "service_role";
GRANT SELECT ON TABLE "execution"."artifacts" TO "authenticated";



GRANT SELECT ON TABLE "execution"."origin_types" TO "authenticated";
GRANT ALL ON TABLE "execution"."origin_types" TO "service_role";



GRANT ALL ON TABLE "execution"."requests" TO "service_role";
GRANT SELECT,INSERT ON TABLE "execution"."requests" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "execution"."runner_device_bindings" TO "service_role";



GRANT ALL ON TABLE "execution"."steps" TO "service_role";
GRANT SELECT ON TABLE "execution"."steps" TO "authenticated";



GRANT SELECT ON TABLE "lenses"."workflow_node_results" TO "authenticated";
GRANT INSERT,UPDATE ON TABLE "lenses"."workflow_node_results" TO "service_role";



GRANT SELECT ON TABLE "execution"."vw_workflow_run_timeline" TO "authenticated";
GRANT SELECT ON TABLE "execution"."vw_workflow_run_timeline" TO "service_role";












GRANT ALL ON TABLE "integrations"."tool_allowlist" TO "service_role";
GRANT SELECT ON TABLE "integrations"."tool_allowlist" TO "authenticated";



GRANT ALL ON TABLE "integrations"."tools" TO "service_role";
GRANT SELECT ON TABLE "integrations"."tools" TO "authenticated";



GRANT ALL ON TABLE "lensers"."badges" TO "service_role";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lensers"."badges" TO "authenticated";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "lensers"."group_members" TO "anon";
GRANT ALL ON TABLE "lensers"."group_members" TO "authenticated";
GRANT ALL ON TABLE "lensers"."group_members" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "lensers"."groups" TO "anon";
GRANT ALL ON TABLE "lensers"."groups" TO "authenticated";
GRANT ALL ON TABLE "lensers"."groups" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "lensers"."preferences" TO "authenticated";
GRANT ALL ON TABLE "lensers"."preferences" TO "service_role";



GRANT ALL ON TABLE "lensers"."social_links" TO "service_role";
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lensers"."social_links" TO "authenticated";



GRANT SELECT ON TABLE "lensers"."vw_lensers_score" TO "authenticated";
GRANT SELECT ON TABLE "lensers"."vw_lensers_score" TO "service_role";



GRANT SELECT ON TABLE "lenses"."capability_index" TO "authenticated";
GRANT ALL ON TABLE "lenses"."capability_index" TO "service_role";



GRANT SELECT ON TABLE "lenses"."contract_channels" TO "authenticated";
GRANT ALL ON TABLE "lenses"."contract_channels" TO "service_role";



GRANT SELECT ON TABLE "lenses"."contract_signatures" TO "authenticated";
GRANT ALL ON TABLE "lenses"."contract_signatures" TO "service_role";



GRANT SELECT ON TABLE "lenses"."contracts" TO "authenticated";
GRANT ALL ON TABLE "lenses"."contracts" TO "service_role";



GRANT SELECT ON TABLE "lenses"."dependency_edges" TO "authenticated";
GRANT ALL ON TABLE "lenses"."dependency_edges" TO "service_role";



GRANT SELECT ON TABLE "lenses"."execution_records" TO "authenticated";
GRANT ALL ON TABLE "lenses"."execution_records" TO "service_role";



GRANT SELECT ON TABLE "lenses"."parameter_contracts" TO "authenticated";
GRANT ALL ON TABLE "lenses"."parameter_contracts" TO "service_role";



GRANT SELECT ON TABLE "lenses"."parameter_deprecations" TO "authenticated";
GRANT ALL ON TABLE "lenses"."parameter_deprecations" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."schedule_calendars" TO "authenticated";
GRANT ALL ON TABLE "lenses"."schedule_calendars" TO "service_role";



GRANT SELECT ON TABLE "lenses"."security_scopes" TO "authenticated";
GRANT ALL ON TABLE "lenses"."security_scopes" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."steps" TO "authenticated";
GRANT ALL ON TABLE "lenses"."steps" TO "service_role";



GRANT SELECT ON TABLE "lenses"."tools" TO "authenticated";



GRANT ALL ON TABLE "lenses"."version_parameters" TO "authenticated";
GRANT ALL ON TABLE "lenses"."version_parameters" TO "service_role";



GRANT SELECT ON TABLE "lenses"."vw_hot_scores" TO "authenticated";
GRANT ALL ON TABLE "lenses"."vw_hot_scores" TO "service_role";



GRANT SELECT ON TABLE "lenses"."vw_lens_version_history" TO "authenticated";
GRANT SELECT ON TABLE "lenses"."vw_lens_version_history" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."workflow_edges" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."workflow_nodes" TO "authenticated";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."workflow_phases" TO "authenticated";
GRANT SELECT ON TABLE "lenses"."workflow_phases" TO "service_role";



GRANT SELECT ON TABLE "lenses"."workflow_run_events" TO "authenticated";
GRANT ALL ON TABLE "lenses"."workflow_run_events" TO "service_role";



GRANT SELECT ON TABLE "lenses"."workflow_run_provenance" TO "authenticated";
GRANT ALL ON TABLE "lenses"."workflow_run_provenance" TO "service_role";



GRANT SELECT ON TABLE "lenses"."workflow_run_tags" TO "authenticated";
GRANT ALL ON TABLE "lenses"."workflow_run_tags" TO "service_role";



GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE "lenses"."workflow_tasks" TO "authenticated";
GRANT SELECT ON TABLE "lenses"."workflow_tasks" TO "service_role";



GRANT SELECT,INSERT ON TABLE "lenses"."workflow_version_edges" TO "authenticated";
GRANT ALL ON TABLE "lenses"."workflow_version_edges" TO "service_role";



GRANT SELECT,INSERT ON TABLE "lenses"."workflow_version_nodes" TO "authenticated";
GRANT ALL ON TABLE "lenses"."workflow_version_nodes" TO "service_role";



GRANT SELECT,INSERT,UPDATE ON TABLE "lenses"."workflow_versions" TO "authenticated";
GRANT ALL ON TABLE "lenses"."workflow_versions" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "media"."attachments" TO "anon";
GRANT ALL ON TABLE "media"."attachments" TO "authenticated";
GRANT ALL ON TABLE "media"."attachments" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "media"."objects" TO "anon";
GRANT ALL ON TABLE "media"."objects" TO "authenticated";
GRANT ALL ON TABLE "media"."objects" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."activity_log" TO "anon";
GRANT ALL ON TABLE "organizations"."activity_log" TO "authenticated";
GRANT ALL ON TABLE "organizations"."activity_log" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."addresses" TO "anon";
GRANT ALL ON TABLE "organizations"."addresses" TO "authenticated";
GRANT ALL ON TABLE "organizations"."addresses" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."audit_logs" TO "anon";
GRANT ALL ON TABLE "organizations"."audit_logs" TO "authenticated";
GRANT ALL ON TABLE "organizations"."audit_logs" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."companies" TO "anon";
GRANT ALL ON TABLE "organizations"."companies" TO "authenticated";
GRANT ALL ON TABLE "organizations"."companies" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "organizations"."preferences" TO "anon";
GRANT ALL ON TABLE "organizations"."preferences" TO "authenticated";
GRANT ALL ON TABLE "organizations"."preferences" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."contact_messages" TO "anon";
GRANT ALL ON TABLE "public"."contact_messages" TO "authenticated";
GRANT ALL ON TABLE "public"."contact_messages" TO "service_role";



GRANT ALL ON TABLE "public"."notification_aggregates" TO "anon";
GRANT ALL ON TABLE "public"."notification_aggregates" TO "authenticated";
GRANT ALL ON TABLE "public"."notification_aggregates" TO "service_role";



GRANT ALL ON TABLE "public"."notification_preferences" TO "anon";
GRANT ALL ON TABLE "public"."notification_preferences" TO "authenticated";
GRANT ALL ON TABLE "public"."notification_preferences" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."notifications" TO "anon";
GRANT ALL ON TABLE "public"."notifications" TO "authenticated";
GRANT ALL ON TABLE "public"."notifications" TO "service_role";



GRANT ALL ON TABLE "public"."user_oauth_connections" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."v_workflow_run_cost_breakdown" TO "anon";
GRANT ALL ON TABLE "public"."v_workflow_run_cost_breakdown" TO "authenticated";
GRANT ALL ON TABLE "public"."v_workflow_run_cost_breakdown" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."v_workflow_run_health" TO "anon";
GRANT ALL ON TABLE "public"."v_workflow_run_health" TO "authenticated";
GRANT ALL ON TABLE "public"."v_workflow_run_health" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."v_workflow_run_timeline" TO "anon";
GRANT ALL ON TABLE "public"."v_workflow_run_timeline" TO "authenticated";
GRANT ALL ON TABLE "public"."v_workflow_run_timeline" TO "service_role";



GRANT ALL ON TABLE "public"."vw_ai_models_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_ai_models_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_ai_models_public" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."vw_auth_lenser" TO "anon";
GRANT ALL ON TABLE "public"."vw_auth_lenser" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_auth_lenser" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."vw_battle_funnel" TO "anon";
GRANT ALL ON TABLE "public"."vw_battle_funnel" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battle_funnel" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."vw_battle_health" TO "anon";
GRANT ALL ON TABLE "public"."vw_battle_health" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battle_health" TO "service_role";



GRANT ALL ON TABLE "public"."vw_battle_participation" TO "anon";
GRANT ALL ON TABLE "public"."vw_battle_participation" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battle_participation" TO "service_role";



GRANT ALL ON TABLE "public"."vw_battles_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_battles_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battles_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_content_tags_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_content_tags_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_content_tags_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_content_thread_replies_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_content_thread_replies_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_content_thread_replies_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_content_threads_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_content_threads_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_content_threads_public" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."vw_feedback_admin" TO "anon";
GRANT ALL ON TABLE "public"."vw_feedback_admin" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_feedback_admin" TO "service_role";



GRANT ALL ON TABLE "public"."vw_feedback_user" TO "anon";
GRANT ALL ON TABLE "public"."vw_feedback_user" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_feedback_user" TO "service_role";



GRANT ALL ON TABLE "public"."vw_global_messages" TO "anon";
GRANT ALL ON TABLE "public"."vw_global_messages" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_global_messages" TO "service_role";



GRANT ALL ON TABLE "xp"."levels" TO "service_role";
GRANT SELECT ON TABLE "xp"."levels" TO "authenticated";



GRANT ALL ON TABLE "public"."vw_lensers_public_recent" TO "anon";
GRANT ALL ON TABLE "public"."vw_lensers_public_recent" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lensers_public_recent" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lensers_social_links_private" TO "anon";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_private" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_private" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lensers_social_links_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lenses_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_lenses_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lenses_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_tags_public_extended" TO "anon";
GRANT ALL ON TABLE "public"."vw_tags_public_extended" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_tags_public_extended" TO "service_role";



GRANT ALL ON TABLE "public"."vw_tags_public_stats" TO "anon";
GRANT ALL ON TABLE "public"."vw_tags_public_stats" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_tags_public_stats" TO "service_role";



GRANT ALL ON TABLE "public"."vw_workflows" TO "anon";
GRANT ALL ON TABLE "public"."vw_workflows" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_workflows" TO "service_role";



GRANT ALL ON TABLE "public"."vw_xp_leaderboard_global" TO "anon";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_global" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_global" TO "service_role";



GRANT ALL ON TABLE "xp"."season_totals" TO "service_role";
GRANT SELECT ON TABLE "xp"."season_totals" TO "authenticated";



GRANT ALL ON TABLE "xp"."seasons" TO "service_role";
GRANT SELECT ON TABLE "xp"."seasons" TO "authenticated";



GRANT ALL ON TABLE "public"."vw_xp_leaderboard_season" TO "anon";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_season" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_season" TO "service_role";



GRANT ALL ON TABLE "public"."workflow_integration_credentials" TO "anon";
GRANT ALL ON TABLE "public"."workflow_integration_credentials" TO "authenticated";
GRANT ALL ON TABLE "public"."workflow_integration_credentials" TO "service_role";



GRANT ALL ON TABLE "public"."workflow_kv_store" TO "anon";
GRANT ALL ON TABLE "public"."workflow_kv_store" TO "authenticated";
GRANT ALL ON TABLE "public"."workflow_kv_store" TO "service_role";



GRANT ALL ON TABLE "public"."workflow_webhook_triggers" TO "anon";
GRANT ALL ON TABLE "public"."workflow_webhook_triggers" TO "authenticated";
GRANT ALL ON TABLE "public"."workflow_webhook_triggers" TO "service_role";



GRANT SELECT ON TABLE "reputation"."v_trueskill_leaderboard" TO "authenticated";
GRANT SELECT ON TABLE "reputation"."v_trueskill_leaderboard" TO "service_role";



GRANT SELECT ON TABLE "status"."v_public_components" TO "anon";
GRANT SELECT ON TABLE "status"."v_public_components" TO "authenticated";



GRANT SELECT ON TABLE "status"."v_public_incident_updates" TO "anon";
GRANT SELECT ON TABLE "status"."v_public_incident_updates" TO "authenticated";



GRANT SELECT ON TABLE "status"."v_public_incidents" TO "anon";
GRANT SELECT ON TABLE "status"."v_public_incidents" TO "authenticated";



GRANT SELECT ON TABLE "status"."v_public_maintenance_windows" TO "anon";
GRANT SELECT ON TABLE "status"."v_public_maintenance_windows" TO "authenticated";



GRANT SELECT ON TABLE "status"."v_public_services" TO "anon";
GRANT SELECT ON TABLE "status"."v_public_services" TO "authenticated";



GRANT ALL ON TABLE "tenancy"."workspace_members" TO "anon";
GRANT ALL ON TABLE "tenancy"."workspace_members" TO "authenticated";
GRANT ALL ON TABLE "tenancy"."workspace_members" TO "service_role";



GRANT ALL ON TABLE "tenancy"."workspaces" TO "anon";
GRANT ALL ON TABLE "tenancy"."workspaces" TO "authenticated";
GRANT ALL ON TABLE "tenancy"."workspaces" TO "service_role";









GRANT ALL ON TABLE "wallet"."accounts" TO "service_role";



GRANT ALL ON TABLE "wallet"."organization_accounts" TO "anon";
GRANT ALL ON TABLE "wallet"."organization_accounts" TO "authenticated";
GRANT ALL ON TABLE "wallet"."organization_accounts" TO "service_role";



GRANT ALL ON TABLE "wallet"."pending_charges" TO "service_role";



GRANT ALL ON TABLE "wallet"."spending_limits" TO "service_role";



GRANT ALL ON TABLE "wallet"."transactions" TO "service_role";



GRANT SELECT ON TABLE "xp"."apps" TO "authenticated";
GRANT ALL ON TABLE "xp"."apps" TO "service_role";



GRANT SELECT ON TABLE "xp"."contributions" TO "authenticated";
GRANT ALL ON TABLE "xp"."contributions" TO "service_role";



GRANT SELECT ON TABLE "xp"."difficulty_multipliers" TO "authenticated";
GRANT ALL ON TABLE "xp"."difficulty_multipliers" TO "service_role";



GRANT ALL ON TABLE "xp"."event_verifications" TO "service_role";
GRANT SELECT ON TABLE "xp"."event_verifications" TO "authenticated";



GRANT ALL ON TABLE "xp"."events" TO "service_role";
GRANT SELECT ON TABLE "xp"."events" TO "authenticated";



GRANT ALL ON TABLE "xp"."level_ups" TO "service_role";
GRANT SELECT ON TABLE "xp"."level_ups" TO "authenticated";



GRANT ALL ON TABLE "xp"."monthly_rollup" TO "service_role";
GRANT SELECT ON TABLE "xp"."monthly_rollup" TO "authenticated";



GRANT ALL ON TABLE "xp"."policy" TO "service_role";
GRANT SELECT ON TABLE "xp"."policy" TO "authenticated";



GRANT ALL ON TABLE "xp"."rules" TO "service_role";
GRANT SELECT ON TABLE "xp"."rules" TO "authenticated";



GRANT ALL ON TABLE "xp"."streaks" TO "service_role";
GRANT SELECT ON TABLE "xp"."streaks" TO "authenticated";



GRANT ALL ON SEQUENCE "xp"."xp_levels_id_seq" TO "anon";
GRANT ALL ON SEQUENCE "xp"."xp_levels_id_seq" TO "authenticated";
GRANT ALL ON SEQUENCE "xp"."xp_levels_id_seq" TO "service_role";



ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "battles" GRANT ALL ON FUNCTIONS TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "battles" GRANT ALL ON FUNCTIONS TO "service_role";



ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "battles" GRANT ALL ON TABLES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "battles" GRANT SELECT,INSERT,DELETE,UPDATE ON TABLES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "battles" GRANT ALL ON TABLES TO "service_role";



ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";






ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";






ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";






ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "xp" GRANT ALL ON FUNCTIONS TO "service_role";




























drop policy "cities_select_public" on "core"."cities";

drop policy "countries_select_public" on "core"."countries";

drop policy "currencies_select_public" on "core"."currencies";

drop policy "regions_select_public" on "core"."regions";

drop policy "states_select_public" on "core"."states";

drop policy "subregions_select_public" on "core"."subregions";

drop policy "public read components" on "status"."components";

drop policy "public read daily_snapshots" on "status"."daily_snapshots";

drop policy "public read incident_updates" on "status"."incident_updates";

drop policy "public read incidents" on "status"."incidents";

drop policy "public read maintenance_windows" on "status"."maintenance_windows";

drop policy "public read services" on "status"."services";

drop policy "public read uptime_rollups" on "status"."uptime_rollups";

revoke select on table "public"."notifications" from "anon";

revoke delete on table "public"."user_oauth_connections" from "anon";

revoke insert on table "public"."user_oauth_connections" from "anon";

revoke references on table "public"."user_oauth_connections" from "anon";

revoke select on table "public"."user_oauth_connections" from "anon";

revoke trigger on table "public"."user_oauth_connections" from "anon";

revoke truncate on table "public"."user_oauth_connections" from "anon";

revoke update on table "public"."user_oauth_connections" from "anon";

revoke delete on table "public"."user_oauth_connections" from "authenticated";

revoke insert on table "public"."user_oauth_connections" from "authenticated";

revoke references on table "public"."user_oauth_connections" from "authenticated";

revoke select on table "public"."user_oauth_connections" from "authenticated";

revoke trigger on table "public"."user_oauth_connections" from "authenticated";

revoke truncate on table "public"."user_oauth_connections" from "authenticated";

revoke update on table "public"."user_oauth_connections" from "authenticated";


  create policy "cities_select_public"
  on "core"."cities"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "countries_select_public"
  on "core"."countries"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "currencies_select_public"
  on "core"."currencies"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "regions_select_public"
  on "core"."regions"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "states_select_public"
  on "core"."states"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "subregions_select_public"
  on "core"."subregions"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "public read components"
  on "status"."components"
  as permissive
  for select
  to anon, authenticated
using (((is_public = true) AND (EXISTS ( SELECT 1
   FROM status.services s
  WHERE ((s.id = components.service_id) AND (s.is_public = true))))));



  create policy "public read daily_snapshots"
  on "status"."daily_snapshots"
  as permissive
  for select
  to anon, authenticated
using (true);



  create policy "public read incident_updates"
  on "status"."incident_updates"
  as permissive
  for select
  to anon, authenticated
using ((EXISTS ( SELECT 1
   FROM (status.incidents i
     LEFT JOIN status.services s ON ((s.id = i.service_id)))
  WHERE ((i.id = incident_updates.incident_id) AND (i.published = true) AND ((i.service_id IS NULL) OR (s.is_public = true))))));



  create policy "public read incidents"
  on "status"."incidents"
  as permissive
  for select
  to anon, authenticated
using (((published = true) AND ((service_id IS NULL) OR (EXISTS ( SELECT 1
   FROM status.services s
  WHERE ((s.id = incidents.service_id) AND (s.is_public = true)))))));



  create policy "public read maintenance_windows"
  on "status"."maintenance_windows"
  as permissive
  for select
  to anon, authenticated
using (((published = true) AND ((service_id IS NULL) OR (EXISTS ( SELECT 1
   FROM status.services s
  WHERE ((s.id = maintenance_windows.service_id) AND (s.is_public = true)))))));



  create policy "public read services"
  on "status"."services"
  as permissive
  for select
  to anon, authenticated
using ((is_public = true));



  create policy "public read uptime_rollups"
  on "status"."uptime_rollups"
  as permissive
  for select
  to anon, authenticated
using (true);


CREATE TRIGGER trg_login_consolidated AFTER UPDATE OF last_sign_in_at ON auth.users FOR EACH ROW EXECUTE FUNCTION lensers.trg_log_login_from_auth();


  create policy "auth_delete_own_objects"
  on "storage"."objects"
  as permissive
  for delete
  to authenticated
using (((storage.foldername(name))[1] = (auth.uid())::text));



  create policy "auth_read_own_artifacts"
  on "storage"."objects"
  as permissive
  for select
  to authenticated
using (((bucket_id = 'artifacts'::text) AND ((storage.foldername(name))[1] = (auth.uid())::text)));



  create policy "auth_read_own_lens_resources"
  on "storage"."objects"
  as permissive
  for select
  to authenticated
using (((bucket_id = 'lens-resources'::text) AND ((storage.foldername(name))[1] = (auth.uid())::text)));



  create policy "auth_read_own_user_media"
  on "storage"."objects"
  as permissive
  for select
  to authenticated
using (((bucket_id = 'user-media'::text) AND ((storage.foldername(name))[1] = (auth.uid())::text)));



  create policy "auth_upload_artifacts"
  on "storage"."objects"
  as permissive
  for insert
  to authenticated
with check ((bucket_id = 'artifacts'::text));



  create policy "auth_upload_lens_resources"
  on "storage"."objects"
  as permissive
  for insert
  to authenticated
with check ((bucket_id = 'lens-resources'::text));



  create policy "auth_upload_public_assets"
  on "storage"."objects"
  as permissive
  for insert
  to authenticated
with check ((bucket_id = 'public-assets'::text));



  create policy "auth_upload_user_media"
  on "storage"."objects"
  as permissive
  for insert
  to authenticated
with check ((bucket_id = 'user-media'::text));



  create policy "battles_media_read_published"
  on "storage"."objects"
  as permissive
  for select
  to authenticated
using (((bucket_id = 'battles-media'::text) AND (EXISTS ( SELECT 1
   FROM battles.battles b
  WHERE (((b.id)::text = (storage.foldername(objects.name))[1]) AND ((b.status)::text = ANY (ARRAY['voting'::text, 'scoring'::text, 'closed'::text, 'published'::text])))))));



  create policy "battles_media_upload"
  on "storage"."objects"
  as permissive
  for insert
  to authenticated
with check (((bucket_id = 'battles-media'::text) AND (auth.uid() IS NOT NULL) AND (EXISTS ( SELECT 1
   FROM (battles.contenders c
     JOIN battles.contender_entity_map cem ON ((cem.contender_id = c.id)))
  WHERE (((c.battle_id)::text = (storage.foldername(objects.name))[1]) AND ((c.id)::text = (storage.foldername(objects.name))[2]) AND (cem.profile_id = auth.uid()))))));



  create policy "generated_media_owner_read"
  on "storage"."objects"
  as permissive
  for select
  to authenticated
using (((bucket_id = 'generated-media'::text) AND (EXISTS ( SELECT 1
   FROM (media.objects mo
     JOIN lensers.profiles p ON ((p.id = mo.owner_lenser_id)))
  WHERE ((mo.bucket = 'generated-media'::text) AND (mo.object_key = objects.name) AND (p.user_id = auth.uid()))))));



