-- Minimum additive LivingIP V3 epistemic contract for the canonical KB tables.
--
-- This is a compatibility migration, not a production rollout. Existing rows
-- remain in the visible legacy tier. Only rows explicitly linked to an accepted
-- proposal are required to satisfy the modern provenance contract.
--
-- CUTOVER ORDER: deploy the cutover-aware writer, stop the default V2 worker,
-- ALTER ROLE kb_apply NOLOGIN, and end every kb_apply session before running this
-- migration. Approved historical V2 proposals remain review-visible, but the V3
-- contract blocks their execution until each packet is migrated or rejected.

\set ON_ERROR_STOP on
set timezone = 'UTC';

begin;

-- Stable bigint key: controlled V3 function/trigger DDL is exclusive while
-- authorized applies hold the shared transaction form through commit/rollback.
select pg_catalog.pg_advisory_xact_lock(6072343533146485505);

-- The worker mirrors this declaration, and tests require byte-equivalent
-- catalog predicates in preflight/postflight. SHA-256 pins every guard body.
do $trigger_contract_declaration$
begin
  perform pg_catalog.set_config(
    'teleo.v3_expected_trigger_contract',
    $trigger_contract$
[
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_enforce_apply_contract_cutover",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": true,
    "function_schema": "kb_stage",
    "function_security_definer": true,
    "function_source_sha256": "3ab2d5e1d5d1d18b13579fe2c0cae487036c7b147cf7f1b6b9a3633274756c18",
    "schema_name": "kb_stage",
    "table_name": "kb_proposals",
    "trigger_name": "teleo_v3_00_enforce_apply_contract_cutover",
    "trigger_type": 23,
    "update_columns": [
      "payload",
      "proposal_type",
      "status"
    ],
    "when_expression": null
  },
  {
    "argument_count": 2,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f69640061646d697373696f6e5f737461746500",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_linked_insert",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d8c190bd2fc90159b9a4824015c10a73a701a263bba90b67f8ccce75e454e6f8",
    "schema_name": "public",
    "table_name": "claims",
    "trigger_name": "teleo_v3_00_claim_require_linked_insert",
    "trigger_type": 7,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_accepted_proposal",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": true,
    "function_schema": "kb_stage",
    "function_security_definer": true,
    "function_source_sha256": "ca398ef2f185488c0712bd5ebc22402ab9e41e5dfd2f5823b367e6a382e8a64f",
    "schema_name": "public",
    "table_name": "claims",
    "trigger_name": "teleo_v3_claim_require_accepted_proposal",
    "trigger_type": 23,
    "update_columns": [
      "accepted_by_proposal_id"
    ],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_linked_insert",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d8c190bd2fc90159b9a4824015c10a73a701a263bba90b67f8ccce75e454e6f8",
    "schema_name": "public",
    "table_name": "sources",
    "trigger_name": "teleo_v3_00_source_require_linked_insert",
    "trigger_type": 7,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 20,
    "argument_hex": "6964006f776e65725f6167656e745f6964007479706500746578740070726f706f736974696f6e00626f64795f6d61726b646f776e0073636f7065006163636573735f73636f7065007374617475730061646d697373696f6e5f7374617465007265766973696f6e5f6372697465726961007265766973696f6e5f6b696e6400737570657273656465735f696400737570657273656465645f627900636f6e666964656e6365007461677300637265617465645f627900637265617465645f617400757064617465645f61740061636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_guard_immutable_fields",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d243741e552fe8c232440ffa5f853cf261f4b86b6f32d76f375734a2235c2d13",
    "schema_name": "public",
    "table_name": "claims",
    "trigger_name": "teleo_v3_claim_immutable",
    "trigger_type": 19,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_reject_delete",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "46190723ab4520954858dbd4bb6c9230e68e41c68b5aee3b8c2719e1416de43c",
    "schema_name": "public",
    "table_name": "claims",
    "trigger_name": "teleo_v3_claim_reject_delete",
    "trigger_type": 11,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f66726f6d5f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_linked_insert",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d8c190bd2fc90159b9a4824015c10a73a701a263bba90b67f8ccce75e454e6f8",
    "schema_name": "public",
    "table_name": "claim_evidence",
    "trigger_name": "teleo_v3_00_evidence_require_linked_insert",
    "trigger_type": 7,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_accepted_proposal",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": true,
    "function_schema": "kb_stage",
    "function_security_definer": true,
    "function_source_sha256": "ca398ef2f185488c0712bd5ebc22402ab9e41e5dfd2f5823b367e6a382e8a64f",
    "schema_name": "public",
    "table_name": "sources",
    "trigger_name": "teleo_v3_source_require_accepted_proposal",
    "trigger_type": 23,
    "update_columns": [
      "accepted_by_proposal_id"
    ],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_linked_insert",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d8c190bd2fc90159b9a4824015c10a73a701a263bba90b67f8ccce75e454e6f8",
    "schema_name": "public",
    "table_name": "claim_edges",
    "trigger_name": "teleo_v3_00_edge_require_linked_insert",
    "trigger_type": 7,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 18,
    "argument_hex": "696400736f757263655f747970650075726c0073746f726167655f70617468006578636572707400686173680063616e6f6e6963616c5f75726c0073746f726167655f75726900636f6e74656e745f68617368006163636573735f73636f706500696e67657374696f6e5f6f726967696e0070726f76656e616e63655f73746174757300736f757263655f636c617373006175746f5f70726f6d6f74696f6e5f706f6c69637900637265617465645f62790063617074757265645f617400637265617465645f61740061636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_guard_immutable_fields",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d243741e552fe8c232440ffa5f853cf261f4b86b6f32d76f375734a2235c2d13",
    "schema_name": "public",
    "table_name": "sources",
    "trigger_name": "teleo_v3_source_immutable",
    "trigger_type": 19,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_reject_delete",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "46190723ab4520954858dbd4bb6c9230e68e41c68b5aee3b8c2719e1416de43c",
    "schema_name": "public",
    "table_name": "sources",
    "trigger_name": "teleo_v3_source_reject_delete",
    "trigger_type": 11,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f66726f6d5f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_accepted_proposal",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": true,
    "function_schema": "kb_stage",
    "function_security_definer": true,
    "function_source_sha256": "ca398ef2f185488c0712bd5ebc22402ab9e41e5dfd2f5823b367e6a382e8a64f",
    "schema_name": "public",
    "table_name": "claim_evidence",
    "trigger_name": "teleo_v3_evidence_require_accepted_proposal",
    "trigger_type": 23,
    "update_columns": [
      "accepted_from_proposal_id"
    ],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_validate_claim_evidence",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "4c697c497699760214aca99f255c215e393319fd4482d4b36efac839280c3733",
    "schema_name": "public",
    "table_name": "claim_evidence",
    "trigger_name": "teleo_v3_evidence_validate_provenance",
    "trigger_type": 23,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 13,
    "argument_hex": "696400636c61696d5f696400736f757263655f696400726f6c65007765696768740065786365727074006c6f6361746f725f6a736f6e00736f757263655f636f6e74656e745f6861736800726174696f6e616c6500706f6c617269747900637265617465645f627900637265617465645f61740061636365707465645f66726f6d5f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_guard_immutable_fields",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d243741e552fe8c232440ffa5f853cf261f4b86b6f32d76f375734a2235c2d13",
    "schema_name": "public",
    "table_name": "claim_evidence",
    "trigger_name": "teleo_v3_evidence_immutable",
    "trigger_type": 19,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_reject_delete",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "46190723ab4520954858dbd4bb6c9230e68e41c68b5aee3b8c2719e1416de43c",
    "schema_name": "public",
    "table_name": "claim_evidence",
    "trigger_name": "teleo_v3_evidence_reject_delete",
    "trigger_type": 11,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_accepted_proposal",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": true,
    "function_schema": "kb_stage",
    "function_security_definer": true,
    "function_source_sha256": "ca398ef2f185488c0712bd5ebc22402ab9e41e5dfd2f5823b367e6a382e8a64f",
    "schema_name": "public",
    "table_name": "claim_edges",
    "trigger_name": "teleo_v3_edge_require_accepted_proposal",
    "trigger_type": 23,
    "update_columns": [
      "accepted_by_proposal_id"
    ],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_validate_claim_edge",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "6a7a3e9d1d5ac6e9490abc9b7b1f321bb2e0948aea2671415aad28101f4ee936",
    "schema_name": "public",
    "table_name": "claim_edges",
    "trigger_name": "teleo_v3_edge_validate_endpoints",
    "trigger_type": 23,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 11,
    "argument_hex": "69640066726f6d5f636c61696d00746f5f636c61696d00656467655f74797065007765696768740072656c6174696f6e5f7479706500726174696f6e616c650073636f70655f6f725f636f6e646974696f6e7300637265617465645f627900637265617465645f61740061636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_guard_immutable_fields",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d243741e552fe8c232440ffa5f853cf261f4b86b6f32d76f375734a2235c2d13",
    "schema_name": "public",
    "table_name": "claim_edges",
    "trigger_name": "teleo_v3_edge_immutable",
    "trigger_type": 19,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_reject_delete",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "46190723ab4520954858dbd4bb6c9230e68e41c68b5aee3b8c2719e1416de43c",
    "schema_name": "public",
    "table_name": "claim_edges",
    "trigger_name": "teleo_v3_edge_reject_delete",
    "trigger_type": 11,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 1,
    "argument_hex": "61636365707465645f62795f70726f706f73616c5f696400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_require_accepted_proposal",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": true,
    "function_schema": "kb_stage",
    "function_security_definer": true,
    "function_source_sha256": "ca398ef2f185488c0712bd5ebc22402ab9e41e5dfd2f5823b367e6a382e8a64f",
    "schema_name": "public",
    "table_name": "claim_evidence_assessments",
    "trigger_name": "teleo_v3_assessment_require_accepted_proposal",
    "trigger_type": 23,
    "update_columns": [
      "accepted_by_proposal_id"
    ],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_validate_evidence_assessment",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "860f939b4878abe8e2e604e03be9eeb6c23b4878af4b19f9ec42de4b821fac61",
    "schema_name": "public",
    "table_name": "claim_evidence_assessments",
    "trigger_name": "teleo_v3_assessment_validate",
    "trigger_type": 23,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 10,
    "argument_hex": "696400636c61696d5f696400737570706f72745f74696572006368616c6c656e67655f74696572006f766572616c6c5f737461746500726174696f6e616c650065766964656e63655f7365745f686173680061636365707465645f62795f70726f706f73616c5f696400737570657273656465735f6173736573736d656e745f696400637265617465645f617400",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_guard_immutable_fields",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "d243741e552fe8c232440ffa5f853cf261f4b86b6f32d76f375734a2235c2d13",
    "schema_name": "public",
    "table_name": "claim_evidence_assessments",
    "trigger_name": "teleo_v3_assessment_immutable",
    "trigger_type": 19,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_reject_delete",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "46190723ab4520954858dbd4bb6c9230e68e41c68b5aee3b8c2719e1416de43c",
    "schema_name": "public",
    "table_name": "claim_evidence_assessments",
    "trigger_name": "teleo_v3_assessment_reject_delete",
    "trigger_type": 11,
    "update_columns": [],
    "when_expression": null
  },
  {
    "argument_count": 0,
    "argument_hex": "",
    "function_config": [
      "search_path=pg_catalog, pg_temp"
    ],
    "function_identity_arguments": "",
    "function_name": "teleo_v3_guard_linked_proposal_decision",
    "function_owner": "kb_gate_owner",
    "function_owner_only_execute": null,
    "function_schema": "kb_stage",
    "function_security_definer": false,
    "function_source_sha256": "602ee8f81f4515b0237f09a503ecbfe4fce2dc4a799e13f833554103b0e83e32",
    "schema_name": "kb_stage",
    "table_name": "kb_proposals",
    "trigger_name": "teleo_v3_guard_linked_proposal_decision",
    "trigger_type": 19,
    "update_columns": [
      "applied_at",
      "applied_by_agent_id",
      "applied_by_handle",
      "payload",
      "proposal_type",
      "rationale",
      "review_note",
      "reviewed_at",
      "reviewed_by_agent_id",
      "reviewed_by_handle",
      "status"
    ],
    "when_expression": null
  }
]
$trigger_contract$,
    true
  );
end
$trigger_contract_declaration$;


do $preflight$
declare
  v_contract_state text;
  v_apply_can_login boolean;
  v_legacy_queue_count bigint;
  v_preexisting_objects text;
  v_ownership_valid boolean;
begin
  if to_regclass('public.agents') is null then
    raise exception 'teleo_v3_epistemic_contract: missing public.agents';
  end if;
  if to_regclass('public.claims') is null then
    raise exception 'teleo_v3_epistemic_contract: missing public.claims';
  end if;
  if to_regclass('public.sources') is null then
    raise exception 'teleo_v3_epistemic_contract: missing public.sources';
  end if;
  if to_regclass('public.claim_evidence') is null then
    raise exception 'teleo_v3_epistemic_contract: missing public.claim_evidence';
  end if;
  if to_regclass('public.claim_edges') is null then
    raise exception 'teleo_v3_epistemic_contract: missing public.claim_edges';
  end if;
  if to_regclass('kb_stage.kb_proposals') is null then
    raise exception 'teleo_v3_epistemic_contract: missing kb_stage.kb_proposals';
  end if;
  if not exists (
    select 1 from pg_catalog.pg_roles role
     where role.rolname = 'kb_gate_owner'
       and not role.rolcanlogin
       and not role.rolinherit
       and not role.rolsuper
       and not role.rolcreatedb
       and not role.rolcreaterole
       and not role.rolreplication
       and not role.rolbypassrls
  ) then
    raise exception using
      errcode = '55000',
      message = 'teleo_v3_epistemic_contract: kb_gate_owner must be NOLOGIN NOINHERIT and unprivileged';
  end if;
  if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_apply') then
    raise exception 'teleo_v3_epistemic_contract: missing runtime role kb_apply';
  end if;
  if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_review') then
    raise exception 'teleo_v3_epistemic_contract: missing runtime role kb_review';
  end if;
  if exists (
    select 1
      from pg_catalog.pg_auth_members membership
      join pg_catalog.pg_roles member_role on member_role.oid = membership.member
      join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
     where member_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
        or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
  ) then
    raise exception using
      errcode = '55000',
      message = 'teleo_v3_epistemic_contract: protected role membership drift must be removed';
  end if;
  if not pg_catalog.has_schema_privilege('kb_gate_owner', 'kb_stage', 'USAGE')
     or not pg_catalog.has_table_privilege('kb_gate_owner', 'kb_stage.kb_proposals', 'SELECT')
     or not pg_catalog.has_table_privilege('kb_gate_owner', 'kb_stage.kb_proposals', 'UPDATE') then
    raise exception 'teleo_v3_epistemic_contract: kb_gate_owner lacks narrow proposal-ledger lock access';
  end if;

  -- TELEO_V3_CONTRACT_STATE_QUERY_BEGIN
with expected_triggers as (
  select expected.*
    from pg_catalog.jsonb_to_recordset(pg_catalog.current_setting('teleo.v3_expected_trigger_contract')::jsonb) as expected(
      schema_name text,
      table_name text,
      trigger_name text,
      trigger_type integer,
      update_columns jsonb,
      when_expression text,
      argument_count integer,
      argument_hex text,
      function_schema text,
      function_name text,
      function_identity_arguments text,
      function_owner text,
      function_security_definer boolean,
      function_config jsonb,
      function_source_sha256 text,
      function_owner_only_execute boolean
    )
), expected_functions as (
  select distinct function_schema, function_name, function_identity_arguments,
         function_owner, function_security_definer, function_config,
         function_source_sha256, function_owner_only_execute
    from expected_triggers
), actual_v3_functions as (
  select procedure.oid, namespace.nspname as function_schema,
         procedure.proname as function_name,
         pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
         owner_role.rolname as function_owner,
         procedure.prosecdef as function_security_definer,
         pg_catalog.to_jsonb(procedure.proconfig) as function_config,
         pg_catalog.encode(
           pg_catalog.sha256(pg_catalog.convert_to(procedure.prosrc, 'UTF8')),
           'hex'
         ) as function_source_sha256,
         not exists (
           select 1
             from pg_catalog.aclexplode(
               coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
             ) acl
            where acl.privilege_type = 'EXECUTE'
              and acl.grantee <> procedure.proowner
         ) as function_owner_only_execute,
         language.lanname as function_language,
         procedure.prokind as function_kind,
         procedure.pronargs as function_argument_count,
         procedure.prorettype
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
   join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
    join pg_catalog.pg_language language on language.oid = procedure.prolang
   where namespace.nspname = 'kb_stage'
     and pg_catalog.left(procedure.proname, 9) = E'teleo_v3_'
     and procedure.proname <> E'teleo_v3_acquire_protected_relation_locks'
), actual_v3_catalog_lock_functions as (
  select procedure.oid,
         owner_role.rolname as function_owner,
         procedure.prosecdef as function_security_definer,
         pg_catalog.to_jsonb(procedure.proconfig) as function_config,
         pg_catalog.encode(
           pg_catalog.sha256(pg_catalog.convert_to(procedure.prosrc, 'UTF8')),
           'hex'
         ) as function_source_sha256,
         language.lanname as function_language,
         procedure.prokind as function_kind,
         procedure.pronargs as function_argument_count,
         pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
         procedure.prorettype,
         exists (
           select 1
             from pg_catalog.aclexplode(
               coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
             ) acl
             join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
            where acl.privilege_type = 'EXECUTE'
              and grantee.rolname = 'kb_apply'
         ) as apply_execute,
         not exists (
           select 1
             from pg_catalog.aclexplode(
               coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
             ) acl
            where acl.privilege_type = 'EXECUTE'
              and acl.grantee <> procedure.proowner
              and acl.grantee <> (
                select role.oid from pg_catalog.pg_roles role where role.rolname = 'kb_apply'
              )
         ) as execute_acl_exact
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
    join pg_catalog.pg_language language on language.oid = procedure.prolang
   where namespace.nspname = 'kb_stage'
     and procedure.proname = E'teleo_v3_acquire_protected_relation_locks'
), actual_v3_triggers as (
  select trigger_row.*, table_namespace.nspname as schema_name,
         relation.relname as table_name,
         function_namespace.nspname as function_schema,
         procedure.proname as function_name,
         pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
         coalesce((
           select pg_catalog.jsonb_agg(attribute.attname order by attribute.attname)
             from pg_catalog.unnest(trigger_row.tgattr::smallint[]) update_column(attnum)
             join pg_catalog.pg_attribute attribute
               on attribute.attrelid = trigger_row.tgrelid
              and attribute.attnum = update_column.attnum
              and not attribute.attisdropped
         ), '[]'::jsonb) as update_columns,
         pg_catalog.pg_get_expr(trigger_row.tgqual, trigger_row.tgrelid, true) as when_expression,
         pg_catalog.encode(trigger_row.tgargs, 'hex') as argument_hex
    from pg_catalog.pg_trigger trigger_row
    join pg_catalog.pg_class relation on relation.oid = trigger_row.tgrelid
    join pg_catalog.pg_namespace table_namespace on table_namespace.oid = relation.relnamespace
    join pg_catalog.pg_proc procedure on procedure.oid = trigger_row.tgfoid
    join pg_catalog.pg_namespace function_namespace on function_namespace.oid = procedure.pronamespace
   where pg_catalog.left(trigger_row.tgname, 9) = E'teleo_v3_'
), function_contract as (
  select
    (select pg_catalog.count(*) from actual_v3_functions) =
      (select pg_catalog.count(*) from expected_functions)
    and not exists (
      select 1
        from expected_functions expected
       where not exists (
         select 1
           from actual_v3_functions actual
          where actual.function_schema = expected.function_schema
            and actual.function_name = expected.function_name
            and actual.function_identity_arguments = expected.function_identity_arguments
            and actual.function_owner = expected.function_owner
            and actual.function_security_definer = expected.function_security_definer
            and actual.function_config = expected.function_config
            and actual.function_source_sha256 = expected.function_source_sha256
            and (
              expected.function_owner_only_execute is null
              or actual.function_owner_only_execute = expected.function_owner_only_execute
            )
            and actual.function_language = 'plpgsql'
            and actual.function_kind = 'f'
            and actual.function_argument_count = 0
            and actual.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype
       )
    ) as valid
), catalog_lock_function_contract as (
  select pg_catalog.count(*) = 1
     and pg_catalog.bool_and(
       function_owner = E'kb_gate_owner'
       and function_security_definer
       and function_config = '["search_path=pg_catalog, pg_temp"]'::jsonb
       and function_source_sha256 = E'897345944ba0f1b2a5c01b098fb17de6f881a122e906277086fa7889e76219e8'
       and function_language = 'plpgsql'
       and function_kind = 'f'
       and function_argument_count = 0
       and function_identity_arguments = ''
       and prorettype = 'pg_catalog.void'::pg_catalog.regtype
       and apply_execute
       and execute_acl_exact
     ) as valid
    from actual_v3_catalog_lock_functions
), trigger_contract as (
  select
    (select pg_catalog.count(*) from actual_v3_triggers) =
      (select pg_catalog.count(*) from expected_triggers)
    and not exists (
      select 1
        from expected_triggers expected
       where not exists (
         select 1
           from actual_v3_triggers actual
          where actual.schema_name = expected.schema_name
            and actual.table_name = expected.table_name
            and actual.tgname = expected.trigger_name
            and not actual.tgisinternal
            and actual.tgenabled = 'O'
            and actual.tgtype = expected.trigger_type
            and actual.update_columns = expected.update_columns
            and actual.when_expression is not distinct from expected.when_expression
            and actual.tgnargs = expected.argument_count
            and actual.argument_hex = expected.argument_hex
            and actual.tgconstraint = 0::pg_catalog.oid
            and not actual.tgdeferrable
            and not actual.tginitdeferred
            and actual.tgconstrrelid = 0::pg_catalog.oid
            and actual.tgparentid = 0::pg_catalog.oid
            and actual.tgoldtable is null
            and actual.tgnewtable is null
            and actual.function_schema = expected.function_schema
            and actual.function_name = expected.function_name
            and actual.function_identity_arguments = expected.function_identity_arguments
            and exists (
              select 1
                from actual_v3_functions function_row
               where function_row.oid = actual.tgfoid
                 and function_row.function_schema = expected.function_schema
                 and function_row.function_name = expected.function_name
                 and function_row.function_identity_arguments = expected.function_identity_arguments
            )
       )
    ) as valid
), catalog_state as (
  select
    exists (select 1 from actual_v3_functions)
      or exists (select 1 from actual_v3_catalog_lock_functions)
      or exists (select 1 from actual_v3_triggers) as artifacts_present,
    (select valid from function_contract) as functions_valid,
    (select valid from catalog_lock_function_contract) as catalog_lock_function_valid,
    (select valid from trigger_contract) as triggers_valid,
    exists (
      select 1
        from pg_catalog.pg_roles owner_role
       where owner_role.rolname = E'kb_gate_owner'
         and not owner_role.rolcanlogin
         and not owner_role.rolinherit
         and not owner_role.rolsuper
         and not owner_role.rolcreatedb
         and not owner_role.rolcreaterole
         and not owner_role.rolreplication
         and not owner_role.rolbypassrls
    ) as owner_role_valid,
    not exists (
      select 1
        from pg_catalog.pg_auth_members membership
        join pg_catalog.pg_roles member_role on member_role.oid = membership.member
        join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
       where member_role.rolname in (E'kb_gate_owner', E'kb_review', E'kb_apply')
          or granted_role.rolname in (E'kb_gate_owner', E'kb_review', E'kb_apply')
    ) as memberships_valid
)
select case
  when not artifacts_present then 'V2'
  when functions_valid and catalog_lock_function_valid and triggers_valid
    and owner_role_valid and memberships_valid then 'V3'
  else 'INVALID'
end
  into v_contract_state
from catalog_state;
  -- TELEO_V3_CONTRACT_STATE_QUERY_END

  if v_contract_state <> 'V3' then
    select role.rolcanlogin into v_apply_can_login
      from pg_catalog.pg_roles role where role.rolname = 'kb_apply';
    if v_apply_can_login then
      raise exception using
        errcode = '55000',
        message = 'teleo_v3 cutover refused before DDL: stop the V2 worker and ALTER ROLE kb_apply NOLOGIN';
    end if;
    if exists (
      select 1 from pg_catalog.pg_stat_activity activity
       where activity.datid = (select oid from pg_catalog.pg_database where datname = current_database())
         and activity.usename = 'kb_apply'
         and activity.pid <> pg_catalog.pg_backend_pid()
    ) then
      raise exception using
        errcode = '55000',
        message = 'teleo_v3 cutover refused before DDL: active kb_apply sessions must exit';
    end if;
  end if;

  select pg_catalog.count(*) into v_legacy_queue_count
    from kb_stage.kb_proposals proposal
   where proposal.status::text in ('pending_review', 'approved')
     and proposal.proposal_type in ('revise_strategy', 'add_edge', 'attach_evidence', 'approve_claim')
     and not (
       proposal.proposal_type = 'approve_claim'
       and pg_catalog.jsonb_typeof(proposal.payload) = 'object'
       and pg_catalog.jsonb_typeof(proposal.payload->'apply_payload') = 'object'
       and pg_catalog.jsonb_typeof(proposal.payload->'apply_payload'->'contract_version') = 'number'
       and proposal.payload->'apply_payload'->>'contract_version' = '3'
     );
  if v_legacy_queue_count > 0 then
    raise notice 'teleo_v3 cutover retains % pending/approved V2 proposal(s) for explicit migration or rejection',
      v_legacy_queue_count;
  end if;

  if not exists (
    select 1 from information_schema.columns
     where table_schema = 'public' and table_name = 'claims' and column_name = 'text'
  ) then
    raise exception 'teleo_v3_epistemic_contract: public.claims.text compatibility column is required';
  end if;
  if not exists (
    select 1 from information_schema.columns
     where table_schema = 'public' and table_name = 'sources' and column_name = 'hash'
  ) then
    raise exception 'teleo_v3_epistemic_contract: public.sources.hash compatibility column is required';
  end if;
  if not exists (
    select 1 from information_schema.columns
     where table_schema = 'public' and table_name = 'claim_edges' and column_name = 'edge_type'
  ) then
    raise exception 'teleo_v3_epistemic_contract: public.claim_edges.edge_type compatibility column is required';
  end if;

  if v_contract_state = 'V2' then
    with expected_columns(table_name, column_name) as (
      values
        ('claims', 'owner_agent_id'),
        ('claims', 'proposition'),
        ('claims', 'body_markdown'),
        ('claims', 'scope'),
        ('claims', 'access_scope'),
        ('claims', 'admission_state'),
        ('claims', 'revision_criteria'),
        ('claims', 'revision_kind'),
        ('claims', 'supersedes_id'),
        ('claims', 'accepted_by_proposal_id'),
        ('sources', 'canonical_url'),
        ('sources', 'storage_uri'),
        ('sources', 'content_hash'),
        ('sources', 'access_scope'),
        ('sources', 'ingestion_origin'),
        ('sources', 'provenance_status'),
        ('sources', 'source_class'),
        ('sources', 'auto_promotion_policy'),
        ('sources', 'accepted_by_proposal_id'),
        ('claim_evidence', 'excerpt'),
        ('claim_evidence', 'locator_json'),
        ('claim_evidence', 'source_content_hash'),
        ('claim_evidence', 'rationale'),
        ('claim_evidence', 'polarity'),
        ('claim_evidence', 'accepted_from_proposal_id'),
        ('claim_edges', 'relation_type'),
        ('claim_edges', 'rationale'),
        ('claim_edges', 'scope_or_conditions'),
        ('claim_edges', 'accepted_by_proposal_id')
    ), present_objects(object_name) as (
      select pg_catalog.format('%I.%I', columns.table_name, columns.column_name)
        from information_schema.columns columns
        join expected_columns expected
          on expected.table_name = columns.table_name
         and expected.column_name = columns.column_name
       where columns.table_schema = 'public'
      union all
      select 'public.claim_evidence_assessments'
       where pg_catalog.to_regclass('public.claim_evidence_assessments') is not null
      union all
      select 'kb_stage.teleo_v3_installation_ownership'
       where pg_catalog.to_regclass('kb_stage.teleo_v3_installation_ownership') is not null
    )
    select pg_catalog.string_agg(object_name, ', ' order by object_name)
      into v_preexisting_objects
      from present_objects;

    if v_preexisting_objects is not null then
      raise exception using
        errcode = '55000',
        message = pg_catalog.format(
          'teleo_v3_epistemic_contract: preexisting V3 schema objects lack installation ownership: %s',
          v_preexisting_objects
        );
    end if;
  elsif v_contract_state in ('V3', 'INVALID') then
    if pg_catalog.to_regclass('kb_stage.teleo_v3_installation_ownership') is null then
      if v_contract_state = 'INVALID' then
        raise exception using
          errcode = '55000',
          message = 'teleo_v3_epistemic_contract: partial or altered V3 contract lacks exact installation ownership';
      end if;
      raise exception using
        errcode = '55000',
        message = 'teleo_v3_epistemic_contract: existing V3 contract lacks exact installation ownership';
    end if;

    select owner_role.rolname = 'kb_gate_owner'
      into v_ownership_valid
      from pg_catalog.pg_class relation
      join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
      join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
     where namespace.nspname = 'kb_stage'
       and relation.relname = 'teleo_v3_installation_ownership'
       and relation.relkind in ('r', 'p');

    if not coalesce(v_ownership_valid, false) then
      raise exception using
        errcode = '55000',
        message = 'teleo_v3_epistemic_contract: installation ownership ledger owner is invalid';
    end if;

    execute $ownership_query$
      select count(*) = 1
         and bool_and(
           contract_name = 'livingip.teleo-v3-epistemic-contract'
           and contract_version = 1
           and installation_token = 'collision-free-install-v1'
         )
        from kb_stage.teleo_v3_installation_ownership
    $ownership_query$ into v_ownership_valid;

    if not coalesce(v_ownership_valid, false) then
      raise exception using
        errcode = '55000',
        message = 'teleo_v3_epistemic_contract: installation ownership ledger is not exact';
    end if;
  end if;
end
$preflight$;

create table if not exists kb_stage.teleo_v3_installation_ownership (
  contract_name text primary key,
  contract_version integer not null,
  installation_token text not null,
  constraint teleo_v3_installation_contract_check check (
    contract_name = 'livingip.teleo-v3-epistemic-contract' and contract_version = 1
  ),
  constraint teleo_v3_installation_token_check check (
    installation_token = 'collision-free-install-v1'
  )
);
alter table kb_stage.teleo_v3_installation_ownership owner to kb_gate_owner;
revoke all privileges on table kb_stage.teleo_v3_installation_ownership
  from public, kb_apply, kb_review;
insert into kb_stage.teleo_v3_installation_ownership (
  contract_name, contract_version, installation_token
) values (
  'livingip.teleo-v3-epistemic-contract',
  1,
  'collision-free-install-v1'
)
on conflict (contract_name) do nothing;

alter table public.claims
  add column if not exists owner_agent_id uuid,
  add column if not exists proposition text,
  add column if not exists body_markdown text,
  add column if not exists scope text,
  add column if not exists access_scope text not null default 'collective_internal',
  add column if not exists admission_state text not null default 'legacy',
  add column if not exists revision_criteria text,
  add column if not exists revision_kind text,
  add column if not exists supersedes_id uuid,
  add column if not exists accepted_by_proposal_id uuid;

alter table public.sources
  add column if not exists canonical_url text,
  add column if not exists storage_uri text,
  add column if not exists content_hash text,
  add column if not exists access_scope text,
  add column if not exists ingestion_origin text,
  add column if not exists provenance_status text,
  add column if not exists source_class text,
  add column if not exists auto_promotion_policy text,
  add column if not exists accepted_by_proposal_id uuid;

alter table public.claim_evidence
  add column if not exists excerpt text,
  add column if not exists locator_json jsonb,
  add column if not exists source_content_hash text,
  add column if not exists rationale text,
  add column if not exists polarity text,
  add column if not exists accepted_from_proposal_id uuid;

alter table public.claim_edges
  add column if not exists relation_type text,
  add column if not exists rationale text,
  add column if not exists scope_or_conditions text,
  add column if not exists accepted_by_proposal_id uuid;

create table if not exists public.claim_evidence_assessments (
  id uuid primary key default gen_random_uuid(),
  claim_id uuid not null references public.claims(id),
  support_tier text,
  challenge_tier text,
  overall_state text not null,
  rationale text not null,
  evidence_set_hash text not null,
  accepted_by_proposal_id uuid not null references kb_stage.kb_proposals(id),
  supersedes_assessment_id uuid references public.claim_evidence_assessments(id),
  created_at timestamptz not null default now(),
  constraint teleo_v3_assessment_support_tier_check check (
    support_tier is null or support_tier in ('none', 'weak', 'moderate', 'strong')
  ),
  constraint teleo_v3_assessment_challenge_tier_check check (
    challenge_tier is null or challenge_tier in ('none', 'weak', 'moderate', 'strong')
  ),
  constraint teleo_v3_assessment_overall_state_check check (
    overall_state in ('insufficient', 'support_dominant', 'challenge_dominant', 'mixed')
  ),
  constraint teleo_v3_assessment_rationale_check check (btrim(rationale) <> ''),
  constraint teleo_v3_assessment_evidence_set_hash_check check (
    evidence_set_hash ~ '^[0-9a-f]{64}$'
  ),
  constraint teleo_v3_assessment_not_self_superseding_check check (
    supersedes_assessment_id is null or supersedes_assessment_id <> id
  )
);

do $constraints$
begin
  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claims'::regclass
       and conname = 'teleo_v3_claim_owner_agent_fkey'
  ) then
    alter table public.claims
      add constraint teleo_v3_claim_owner_agent_fkey
      foreign key (owner_agent_id) references public.agents(id);
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claims'::regclass
       and conname = 'teleo_v3_claim_supersedes_fkey'
  ) then
    alter table public.claims
      add constraint teleo_v3_claim_supersedes_fkey
      foreign key (supersedes_id) references public.claims(id);
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claims'::regclass
       and conname = 'teleo_v3_claim_accepted_proposal_fkey'
  ) then
    alter table public.claims
      add constraint teleo_v3_claim_accepted_proposal_fkey
      foreign key (accepted_by_proposal_id) references kb_stage.kb_proposals(id);
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claims'::regclass
       and conname = 'teleo_v3_claim_admission_check'
  ) then
    alter table public.claims
      add constraint teleo_v3_claim_admission_check check (
        admission_state = 'legacy'
        or (
          admission_state = 'admitted'
          and type in ('empirical', 'predictive', 'causal', 'normative', 'conceptual')
          and proposition is not null and btrim(proposition) <> ''
          and text = proposition
          and body_markdown is not null and btrim(body_markdown) <> ''
          and scope is not null and btrim(scope) <> ''
          and access_scope in ('agent_private', 'collective_internal', 'public')
          and (access_scope <> 'agent_private' or owner_agent_id is not null)
          and revision_criteria is not null and btrim(revision_criteria) <> ''
          and revision_kind is not null
          and revision_kind in ('original', 'editorial', 'semantic', 'metadata_correction')
          and status in ('active', 'contested', 'retracted', 'superseded')
          and accepted_by_proposal_id is not null
          and (
            (revision_kind = 'original' and supersedes_id is null)
            or (revision_kind <> 'original' and supersedes_id is not null)
          )
        )
      );
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.sources'::regclass
       and conname = 'teleo_v3_source_accepted_proposal_fkey'
  ) then
    alter table public.sources
      add constraint teleo_v3_source_accepted_proposal_fkey
      foreign key (accepted_by_proposal_id) references kb_stage.kb_proposals(id);
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.sources'::regclass
       and conname = 'teleo_v3_source_contract_check'
  ) then
    alter table public.sources
      add constraint teleo_v3_source_contract_check check (
        accepted_by_proposal_id is null
        or (
          content_hash is not null
          and content_hash ~ '^[0-9a-f]{64}$'
          and hash = content_hash
          and (
            (canonical_url is not null and btrim(canonical_url) <> '')
            or (storage_uri is not null and btrim(storage_uri) <> '')
          )
          and access_scope is not null
          and access_scope in ('agent_private', 'collective_internal', 'public')
          and ingestion_origin is not null
          and ingestion_origin in ('operator_upload', 'agent_fetch', 'contributor', 'system_import', 'unknown')
          and provenance_status is not null
          and provenance_status in ('verified', 'unverified', 'disputed')
          and source_class is not null
          and source_class in (
            'primary_record', 'peer_reviewed', 'institutional', 'journalistic', 'self_published', 'unknown'
          )
          and auto_promotion_policy is not null
          and auto_promotion_policy in ('eligible', 'human_required', 'blocked')
        )
      );
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claim_evidence'::regclass
       and conname = 'teleo_v3_evidence_accepted_proposal_fkey'
  ) then
    alter table public.claim_evidence
      add constraint teleo_v3_evidence_accepted_proposal_fkey
      foreign key (accepted_from_proposal_id) references kb_stage.kb_proposals(id);
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claim_evidence'::regclass
       and conname = 'teleo_v3_evidence_contract_check'
  ) then
    alter table public.claim_evidence
      add constraint teleo_v3_evidence_contract_check check (
        accepted_from_proposal_id is null
        or (
          polarity is not null
          and polarity in ('supports', 'challenges', 'illustrates')
          and (
            (polarity = 'supports' and role::text = 'grounds')
            or (polarity = 'challenges' and role::text = 'contradicts')
            or (polarity = 'illustrates' and role::text = 'illustrates')
          )
          and locator_json is not null
          and jsonb_typeof(locator_json) = 'object'
          and locator_json <> '{}'::jsonb
          and source_content_hash is not null
          and source_content_hash ~ '^[0-9a-f]{64}$'
          and rationale is not null and btrim(rationale) <> ''
          and weight is null
        )
      );
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claim_edges'::regclass
       and conname = 'teleo_v3_edge_accepted_proposal_fkey'
  ) then
    alter table public.claim_edges
      add constraint teleo_v3_edge_accepted_proposal_fkey
      foreign key (accepted_by_proposal_id) references kb_stage.kb_proposals(id);
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'public.claim_edges'::regclass
       and conname = 'teleo_v3_edge_contract_check'
  ) then
    alter table public.claim_edges
      add constraint teleo_v3_edge_contract_check check (
        accepted_by_proposal_id is null
        or (
          relation_type is not null
          and relation_type in ('supports', 'challenges', 'depends_on', 'contradicts')
          and (
            relation_type = edge_type::text
            or (relation_type = 'depends_on' and edge_type::text = 'requires')
          )
          and rationale is not null and btrim(rationale) <> ''
          and from_claim <> to_claim
          and weight is null
        )
      );
  end if;

  if not exists (
    select 1 from pg_constraint
     where conrelid = 'kb_stage.kb_proposals'::regclass
       and conname = 'teleo_v3_proposal_apply_receipt_check'
  ) then
    alter table kb_stage.kb_proposals
      add constraint teleo_v3_proposal_apply_receipt_check check (
        (
          status::text = 'applied'
          and applied_at is not null
          and (
            applied_by_agent_id is not null
            or nullif(btrim(applied_by_handle), '') is not null
          )
        )
        or (
          status::text <> 'applied'
          and applied_at is null
          and applied_by_agent_id is null
          and nullif(btrim(applied_by_handle), '') is null
        )
      ) not valid;
  end if;
end
$constraints$;

create unique index if not exists teleo_v3_claim_one_admitted_successor_idx
  on public.claims (supersedes_id)
  where admission_state = 'admitted' and supersedes_id is not null;

create unique index if not exists teleo_v3_assessment_one_successor_idx
  on public.claim_evidence_assessments (supersedes_assessment_id)
  where supersedes_assessment_id is not null;

create index if not exists teleo_v3_evidence_claim_polarity_idx
  on public.claim_evidence (claim_id, polarity)
  where accepted_from_proposal_id is not null;

create index if not exists teleo_v3_edge_typed_adjacency_idx
  on public.claim_edges (from_claim, relation_type, to_claim)
  where accepted_by_proposal_id is not null;

-- kb_apply intentionally has SELECT-only access to the proposal ledger, so a
-- fixed SECURITY DEFINER helper takes only that table's transaction-held lock.
-- The apply takes the remaining relation locks directly under its narrow DML
-- grants. The exact helper body, owner, search_path, return type, and ACL are
-- part of the fail-closed catalog predicate above and below.
create or replace function kb_stage.teleo_v3_acquire_protected_relation_locks()
returns void
language plpgsql
security definer
set search_path = pg_catalog, pg_temp
as $function$
begin
  lock table kb_stage.kb_proposals in row exclusive mode;
end
$function$;

alter function kb_stage.teleo_v3_acquire_protected_relation_locks() owner to kb_gate_owner;
revoke all on function kb_stage.teleo_v3_acquire_protected_relation_locks()
  from public, kb_apply, kb_review;
grant execute on function kb_stage.teleo_v3_acquire_protected_relation_locks()
  to kb_apply;

create or replace function kb_stage.teleo_v3_enforce_apply_contract_cutover()
returns trigger
language plpgsql
security definer
set search_path = pg_catalog, pg_temp
as $function$
declare
  v_exact_v3 boolean;
begin
  v_exact_v3 :=
    new.proposal_type = 'approve_claim'
    and pg_catalog.jsonb_typeof(new.payload) = 'object'
    and pg_catalog.jsonb_typeof(new.payload->'apply_payload') = 'object'
    and pg_catalog.jsonb_typeof(new.payload->'apply_payload'->'contract_version') = 'number'
    and new.payload->'apply_payload'->>'contract_version' = '3';

  if v_exact_v3 then
    return new;
  end if;
  if tg_op = 'UPDATE'
     and old.status::text in ('pending_review', 'approved')
     and new.status::text in ('rejected', 'canceled')
     and new.proposal_type is not distinct from old.proposal_type
     and new.payload is not distinct from old.payload then
    return new;
  end if;
  if new.status::text in ('pending_review', 'approved', 'applied')
     and new.proposal_type in ('revise_strategy', 'add_edge', 'attach_evidence', 'approve_claim') then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format(
        'teleo_v3 cutover: proposal %s uses a disabled V2 or malformed apply contract; migrate or reject it',
        new.id
      );
  end if;
  return new;
end
$function$;

alter function kb_stage.teleo_v3_enforce_apply_contract_cutover() owner to kb_gate_owner;
revoke all on function kb_stage.teleo_v3_enforce_apply_contract_cutover()
  from public, kb_apply, kb_review;

create or replace function kb_stage.teleo_v3_require_accepted_proposal()
returns trigger
language plpgsql
security definer
set search_path = pg_catalog, pg_temp
as $function$
declare
  proposal_id uuid;
  proposal_status text;
  proposal_reviewed_by_handle text;
  proposal_reviewed_by_agent_id uuid;
  proposal_reviewed_at timestamptz;
  proposal_review_note text;
  proposal_applied_by_handle text;
  proposal_applied_by_agent_id uuid;
  proposal_applied_at timestamptz;
begin
  proposal_id := nullif(pg_catalog.to_jsonb(new) ->> tg_argv[0], '')::uuid;
  if proposal_id is null then
    return new;
  end if;

  select status::text, reviewed_by_handle, reviewed_by_agent_id, reviewed_at, review_note,
         applied_by_handle, applied_by_agent_id, applied_at
    into proposal_status, proposal_reviewed_by_handle, proposal_reviewed_by_agent_id,
         proposal_reviewed_at, proposal_review_note, proposal_applied_by_handle,
         proposal_applied_by_agent_id, proposal_applied_at
    from kb_stage.kb_proposals
   where id = proposal_id
     for share;

  if not found then
    raise exception using
      errcode = '23503',
      message = pg_catalog.format('teleo_v3: proposal %s does not exist', proposal_id);
  end if;
  if proposal_status not in ('approved', 'accepted', 'applied')
     or proposal_reviewed_at is null
     or (proposal_reviewed_by_agent_id is null and nullif(pg_catalog.btrim(proposal_reviewed_by_handle), '') is null)
     or nullif(pg_catalog.btrim(proposal_review_note), '') is null then
    raise exception using
      errcode = '23514',
      message = pg_catalog.format('teleo_v3: proposal %s is not an accepted reviewed decision', proposal_id);
  end if;
  if proposal_status = 'applied'
     and (
       proposal_applied_at is null
       or (
         proposal_applied_by_agent_id is null
         and nullif(pg_catalog.btrim(proposal_applied_by_handle), '') is null
       )
     ) then
    raise exception using
      errcode = '23514',
      message = pg_catalog.format('teleo_v3: applied proposal %s lacks immutable apply metadata', proposal_id);
  end if;
  if proposal_status <> 'applied'
     and (
       proposal_applied_at is not null
       or proposal_applied_by_agent_id is not null
       or nullif(pg_catalog.btrim(proposal_applied_by_handle), '') is not null
     ) then
    raise exception using
      errcode = '23514',
      message = pg_catalog.format('teleo_v3: non-applied proposal %s carries invalid apply metadata', proposal_id);
  end if;
  return new;
end
$function$;

-- The trigger takes FOR SHARE on the proposal ledger so a decision cannot be
-- downgraded between validation and canonical insertion. Runtime writers do
-- not receive direct UPDATE/row-lock privilege on that ledger; the fixed,
-- trigger-only SECURITY DEFINER body is the sole narrow lock path.
alter function kb_stage.teleo_v3_require_accepted_proposal() owner to kb_gate_owner;
revoke all on function kb_stage.teleo_v3_require_accepted_proposal()
  from public, kb_apply, kb_review;

do $security_definer_invariants$
declare
  v_drift text;
begin
  select pg_catalog.format(
           'owner=%I security_definer=%s config=%s acl=%s',
           owner_role.rolname,
           procedure.prosecdef,
           procedure.proconfig,
           procedure.proacl
         )
    into v_drift
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
   where namespace.nspname = 'kb_stage'
     and procedure.proname in (
       'teleo_v3_enforce_apply_contract_cutover',
       'teleo_v3_require_accepted_proposal'
     )
     and (
       owner_role.rolname <> 'kb_gate_owner'
       or not procedure.prosecdef
       or pg_catalog.array_to_string(procedure.proconfig, ',') is distinct from
          'search_path=pg_catalog, pg_temp'
       or exists (
         select 1
           from pg_catalog.aclexplode(
             coalesce(
               procedure.proacl,
               pg_catalog.acldefault('f', procedure.proowner)
             )
           ) acl
          where acl.privilege_type = 'EXECUTE'
            and acl.grantee <> procedure.proowner
       )
     );
  if v_drift is not null then
    raise exception 'teleo_v3_epistemic_contract: SECURITY DEFINER trigger invariants failed: %', v_drift;
  end if;
end
$security_definer_invariants$;

create or replace function kb_stage.teleo_v3_require_linked_insert()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
declare
  proposal_field text := tg_argv[0];
  admission_field text := case when array_length(tg_argv, 1) > 1 then tg_argv[1] else null end;
  proposal_value text;
  admission_value text;
begin
  proposal_value := pg_catalog.to_jsonb(new) ->> proposal_field;
  if nullif(pg_catalog.btrim(proposal_value), '') is null then
    raise exception using
      errcode = '23514',
      message = pg_catalog.format(
        'teleo_v3: new %s.%s rows must be proposal-linked through %s',
        tg_table_schema, tg_table_name, proposal_field
      );
  end if;
  if admission_field is not null then
    admission_value := pg_catalog.to_jsonb(new) ->> admission_field;
    if admission_value is distinct from 'admitted' then
      raise exception using
        errcode = '23514',
        message = pg_catalog.format(
          'teleo_v3: new %s.%s rows must use %s=admitted; legacy is reserved for rows present at migration',
          tg_table_schema, tg_table_name, admission_field
        );
    end if;
  end if;
  return new;
end
$function$;

create or replace function kb_stage.teleo_v3_guard_immutable_fields()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
declare
  field_name text;
begin
  if (
    tg_table_schema = 'public'
    and tg_table_name = 'claims'
    and (pg_catalog.to_jsonb(old) ->> 'admission_state') is distinct from 'admitted'
  ) or (
    tg_table_schema = 'public'
    and tg_table_name = 'sources'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_by_proposal_id') is null
  ) or (
    tg_table_schema = 'public'
    and tg_table_name = 'claim_evidence'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_from_proposal_id') is null
  ) or (
    tg_table_schema = 'public'
    and tg_table_name = 'claim_edges'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_by_proposal_id') is null
  ) then
    return new;
  end if;

  foreach field_name in array tg_argv loop
    if (pg_catalog.to_jsonb(new) -> field_name) is distinct from (pg_catalog.to_jsonb(old) -> field_name) then
      raise exception using
        errcode = '55000',
        message = pg_catalog.format(
          'teleo_v3: immutable %s.%s field %s requires a new version', tg_table_schema, tg_table_name, field_name
        );
    end if;
  end loop;
  return new;
end
$function$;

create or replace function kb_stage.teleo_v3_reject_delete()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
begin
  if (
    tg_table_schema = 'public'
    and tg_table_name = 'claims'
    and (pg_catalog.to_jsonb(old) ->> 'admission_state') is distinct from 'admitted'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_by_proposal_id') is null
  ) or (
    tg_table_schema = 'public'
    and tg_table_name = 'sources'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_by_proposal_id') is null
  ) or (
    tg_table_schema = 'public'
    and tg_table_name = 'claim_evidence'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_from_proposal_id') is null
  ) or (
    tg_table_schema = 'public'
    and tg_table_name = 'claim_edges'
    and (pg_catalog.to_jsonb(old) ->> 'accepted_by_proposal_id') is null
  ) then
    return old;
  end if;

  raise exception using
    errcode = '55000',
    message = pg_catalog.format(
      'teleo_v3: proposal-linked %s.%s rows are append-only; supersede or retract through a new reviewed version',
      tg_table_schema, tg_table_name
    );
end
$function$;

create or replace function kb_stage.teleo_v3_validate_claim_evidence()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
declare
  claim_admission_state text;
  source_hash text;
  source_proposal_id uuid;
begin
  if new.accepted_from_proposal_id is null then
    return new;
  end if;

  select admission_state into claim_admission_state
    from public.claims where id = new.claim_id;
  select content_hash, accepted_by_proposal_id into source_hash, source_proposal_id
    from public.sources where id = new.source_id;

  if claim_admission_state is distinct from 'admitted' then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: accepted evidence must target an admitted claim version';
  end if;
  if source_proposal_id is null then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: accepted evidence must reference a proposal-linked source version';
  end if;
  if source_hash is distinct from new.source_content_hash then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: evidence source_content_hash does not match the immutable source version';
  end if;
  return new;
end
$function$;

create or replace function kb_stage.teleo_v3_validate_claim_edge()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
declare
  from_admission_state text;
  to_admission_state text;
begin
  if new.accepted_by_proposal_id is null then
    return new;
  end if;

  select admission_state into from_admission_state
    from public.claims where id = new.from_claim;
  select admission_state into to_admission_state
    from public.claims where id = new.to_claim;
  if from_admission_state is distinct from 'admitted' or to_admission_state is distinct from 'admitted' then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: accepted typed edges require two admitted claim versions';
  end if;
  return new;
end
$function$;

create or replace function kb_stage.teleo_v3_validate_evidence_assessment()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
declare
  claim_type text;
  claim_admission_state text;
begin
  select type, admission_state into claim_type, claim_admission_state
    from public.claims where id = new.claim_id;
  if claim_admission_state is distinct from 'admitted' then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: evidence assessments require an admitted claim version';
  end if;
  if claim_type in ('empirical', 'predictive', 'causal')
     and (new.support_tier is null or new.challenge_tier is null) then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: truth-apt claim assessments require support and challenge tiers';
  end if;
  if not exists (
    select 1 from public.claim_evidence
     where claim_id = new.claim_id and accepted_from_proposal_id is not null
  ) then
    raise exception using
      errcode = '23514',
      message = 'teleo_v3: evidence assessments require accepted claim-specific evidence';
  end if;
  return new;
end
$function$;

create or replace function kb_stage.teleo_v3_guard_linked_proposal_decision()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, pg_temp
as $function$
declare
  proposal_is_linked boolean;
  valid_apply_transition boolean;
begin
  select
    exists (select 1 from public.claims where accepted_by_proposal_id = old.id)
    or exists (select 1 from public.sources where accepted_by_proposal_id = old.id)
    or exists (select 1 from public.claim_evidence where accepted_from_proposal_id = old.id)
    or exists (select 1 from public.claim_edges where accepted_by_proposal_id = old.id)
    or exists (select 1 from public.claim_evidence_assessments where accepted_by_proposal_id = old.id)
    into proposal_is_linked;

  valid_apply_transition :=
    old.status::text in ('approved', 'accepted')
    and new.status::text = 'applied'
    and old.applied_by_handle is null
    and old.applied_by_agent_id is null
    and old.applied_at is null
    and new.applied_at is not null
    and (
      new.applied_by_agent_id is not null
      or nullif(pg_catalog.btrim(new.applied_by_handle), '') is not null
    );

  if new.status::text = 'applied' and (
    new.applied_at is null
    or (
      new.applied_by_agent_id is null
      and nullif(pg_catalog.btrim(new.applied_by_handle), '') is null
    )
  ) then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format('teleo_v3: applied proposal %s requires immutable apply metadata', old.id);
  end if;

  if new.status::text <> 'applied' and (
    new.applied_at is not null
    or new.applied_by_agent_id is not null
    or nullif(pg_catalog.btrim(new.applied_by_handle), '') is not null
  ) then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format('teleo_v3: non-applied proposal %s cannot carry apply metadata', old.id);
  end if;

  if old.status::text = 'applied' and (
    new.status::text <> 'applied'
    or new.proposal_type is distinct from old.proposal_type
    or new.payload is distinct from old.payload
    or new.rationale is distinct from old.rationale
    or new.reviewed_by_handle is distinct from old.reviewed_by_handle
    or new.reviewed_by_agent_id is distinct from old.reviewed_by_agent_id
    or new.reviewed_at is distinct from old.reviewed_at
    or new.review_note is distinct from old.review_note
    or new.applied_by_handle is distinct from old.applied_by_handle
    or new.applied_by_agent_id is distinct from old.applied_by_agent_id
    or new.applied_at is distinct from old.applied_at
  ) then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format('teleo_v3: applied proposal %s is immutable', old.id);
  end if;

  if (
    new.applied_by_handle is distinct from old.applied_by_handle
    or new.applied_by_agent_id is distinct from old.applied_by_agent_id
    or new.applied_at is distinct from old.applied_at
  ) and not valid_apply_transition then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format('teleo_v3: proposal %s apply metadata may only be set during apply', old.id);
  end if;

  if proposal_is_linked and (
    new.status::text not in ('approved', 'accepted', 'applied')
    or (
      old.status::text in ('approved', 'accepted')
      and new.status::text not in (old.status::text, 'applied')
    )
    or (old.status::text = 'applied' and new.status::text <> 'applied')
    or new.proposal_type is distinct from old.proposal_type
    or new.payload is distinct from old.payload
    or new.rationale is distinct from old.rationale
    or new.reviewed_by_handle is distinct from old.reviewed_by_handle
    or new.reviewed_by_agent_id is distinct from old.reviewed_by_agent_id
    or new.reviewed_at is distinct from old.reviewed_at
    or new.review_note is distinct from old.review_note
    or new.reviewed_at is null
    or (new.reviewed_by_agent_id is null and nullif(pg_catalog.btrim(new.reviewed_by_handle), '') is null)
    or nullif(pg_catalog.btrim(new.review_note), '') is null
  ) then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format('teleo_v3: linked accepted proposal %s cannot lose its decision metadata', old.id);
  end if;
  return new;
end
$function$;

do $trigger_function_owners$
declare
  v_function record;
begin
  for v_function in
    select distinct expected.function_schema, expected.function_name,
           expected.function_identity_arguments
      from pg_catalog.jsonb_to_recordset(
        pg_catalog.current_setting('teleo.v3_expected_trigger_contract')::jsonb
      ) as expected(
        function_schema text,
        function_name text,
        function_identity_arguments text
      )
  loop
    if v_function.function_identity_arguments <> '' then
      raise exception 'teleo_v3_epistemic_contract: trigger functions must have zero identity arguments';
    end if;
    execute pg_catalog.format(
      'alter function %I.%I() owner to kb_gate_owner',
      v_function.function_schema,
      v_function.function_name
    );
  end loop;
end
$trigger_function_owners$;

do $triggers$
declare
  v_trigger record;
begin
  execute $ddl$
    create or replace trigger teleo_v3_00_enforce_apply_contract_cutover
    before insert or update of proposal_type, payload, status on kb_stage.kb_proposals
    for each row execute function kb_stage.teleo_v3_enforce_apply_contract_cutover()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_00_claim_require_linked_insert
    before insert on public.claims
    for each row execute function kb_stage.teleo_v3_require_linked_insert(
      'accepted_by_proposal_id', 'admission_state'
    )
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_claim_require_accepted_proposal
    before insert or update of accepted_by_proposal_id on public.claims
    for each row execute function kb_stage.teleo_v3_require_accepted_proposal('accepted_by_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_00_source_require_linked_insert
    before insert on public.sources
    for each row execute function kb_stage.teleo_v3_require_linked_insert('accepted_by_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_claim_immutable
    before update on public.claims
    for each row execute function kb_stage.teleo_v3_guard_immutable_fields(
      'id', 'owner_agent_id', 'type', 'text', 'proposition', 'body_markdown', 'scope', 'access_scope',
      'status', 'admission_state', 'revision_criteria', 'revision_kind', 'supersedes_id', 'superseded_by',
      'confidence', 'tags', 'created_by', 'created_at', 'updated_at', 'accepted_by_proposal_id'
    )
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_claim_reject_delete
    before delete on public.claims
    for each row execute function kb_stage.teleo_v3_reject_delete()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_00_evidence_require_linked_insert
    before insert on public.claim_evidence
    for each row execute function kb_stage.teleo_v3_require_linked_insert('accepted_from_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_source_require_accepted_proposal
    before insert or update of accepted_by_proposal_id on public.sources
    for each row execute function kb_stage.teleo_v3_require_accepted_proposal('accepted_by_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_00_edge_require_linked_insert
    before insert on public.claim_edges
    for each row execute function kb_stage.teleo_v3_require_linked_insert('accepted_by_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_source_immutable
    before update on public.sources
    for each row execute function kb_stage.teleo_v3_guard_immutable_fields(
      'id', 'source_type', 'url', 'storage_path', 'excerpt', 'hash', 'canonical_url', 'storage_uri',
      'content_hash', 'access_scope', 'ingestion_origin', 'provenance_status', 'source_class',
      'auto_promotion_policy', 'created_by', 'captured_at', 'created_at', 'accepted_by_proposal_id'
    )
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_source_reject_delete
    before delete on public.sources
    for each row execute function kb_stage.teleo_v3_reject_delete()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_evidence_require_accepted_proposal
    before insert or update of accepted_from_proposal_id on public.claim_evidence
    for each row execute function kb_stage.teleo_v3_require_accepted_proposal('accepted_from_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_evidence_validate_provenance
    before insert or update on public.claim_evidence
    for each row execute function kb_stage.teleo_v3_validate_claim_evidence()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_evidence_immutable
    before update on public.claim_evidence
    for each row execute function kb_stage.teleo_v3_guard_immutable_fields(
      'id', 'claim_id', 'source_id', 'role', 'weight', 'excerpt', 'locator_json', 'source_content_hash',
      'rationale', 'polarity', 'created_by', 'created_at', 'accepted_from_proposal_id'
    )
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_evidence_reject_delete
    before delete on public.claim_evidence
    for each row execute function kb_stage.teleo_v3_reject_delete()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_edge_require_accepted_proposal
    before insert or update of accepted_by_proposal_id on public.claim_edges
    for each row execute function kb_stage.teleo_v3_require_accepted_proposal('accepted_by_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_edge_validate_endpoints
    before insert or update on public.claim_edges
    for each row execute function kb_stage.teleo_v3_validate_claim_edge()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_edge_immutable
    before update on public.claim_edges
    for each row execute function kb_stage.teleo_v3_guard_immutable_fields(
      'id', 'from_claim', 'to_claim', 'edge_type', 'weight', 'relation_type', 'rationale',
      'scope_or_conditions', 'created_by', 'created_at', 'accepted_by_proposal_id'
    )
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_edge_reject_delete
    before delete on public.claim_edges
    for each row execute function kb_stage.teleo_v3_reject_delete()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_assessment_require_accepted_proposal
    before insert or update of accepted_by_proposal_id on public.claim_evidence_assessments
    for each row execute function kb_stage.teleo_v3_require_accepted_proposal('accepted_by_proposal_id')
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_assessment_validate
    before insert or update on public.claim_evidence_assessments
    for each row execute function kb_stage.teleo_v3_validate_evidence_assessment()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_assessment_immutable
    before update on public.claim_evidence_assessments
    for each row execute function kb_stage.teleo_v3_guard_immutable_fields(
      'id', 'claim_id', 'support_tier', 'challenge_tier', 'overall_state', 'rationale',
      'evidence_set_hash', 'accepted_by_proposal_id', 'supersedes_assessment_id', 'created_at'
    )
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_assessment_reject_delete
    before delete on public.claim_evidence_assessments
    for each row execute function kb_stage.teleo_v3_reject_delete()
  $ddl$;

  execute $ddl$
    create or replace trigger teleo_v3_guard_linked_proposal_decision
    before update of proposal_type, status, rationale, payload, reviewed_by_handle,
      reviewed_by_agent_id, reviewed_at, review_note, applied_by_handle, applied_by_agent_id, applied_at
    on kb_stage.kb_proposals
    for each row execute function kb_stage.teleo_v3_guard_linked_proposal_decision()
  $ddl$;

  for v_trigger in
    select expected.schema_name, expected.table_name, expected.trigger_name
      from pg_catalog.jsonb_to_recordset(
        pg_catalog.current_setting('teleo.v3_expected_trigger_contract')::jsonb
      ) as expected(schema_name text, table_name text, trigger_name text)
  loop
    execute pg_catalog.format(
      'alter table %I.%I enable trigger %I',
      v_trigger.schema_name,
      v_trigger.table_name,
      v_trigger.trigger_name
    );
  end loop;
end
$triggers$;

do $contract_postflight$
declare
  v_contract_state text;
begin
  -- TELEO_V3_CONTRACT_POSTFLIGHT_QUERY_BEGIN
with expected_triggers as (
  select expected.*
    from pg_catalog.jsonb_to_recordset(pg_catalog.current_setting('teleo.v3_expected_trigger_contract')::jsonb) as expected(
      schema_name text,
      table_name text,
      trigger_name text,
      trigger_type integer,
      update_columns jsonb,
      when_expression text,
      argument_count integer,
      argument_hex text,
      function_schema text,
      function_name text,
      function_identity_arguments text,
      function_owner text,
      function_security_definer boolean,
      function_config jsonb,
      function_source_sha256 text,
      function_owner_only_execute boolean
    )
), expected_functions as (
  select distinct function_schema, function_name, function_identity_arguments,
         function_owner, function_security_definer, function_config,
         function_source_sha256, function_owner_only_execute
    from expected_triggers
), actual_v3_functions as (
  select procedure.oid, namespace.nspname as function_schema,
         procedure.proname as function_name,
         pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
         owner_role.rolname as function_owner,
         procedure.prosecdef as function_security_definer,
         pg_catalog.to_jsonb(procedure.proconfig) as function_config,
         pg_catalog.encode(
           pg_catalog.sha256(pg_catalog.convert_to(procedure.prosrc, 'UTF8')),
           'hex'
         ) as function_source_sha256,
         not exists (
           select 1
             from pg_catalog.aclexplode(
               coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
             ) acl
            where acl.privilege_type = 'EXECUTE'
              and acl.grantee <> procedure.proowner
         ) as function_owner_only_execute,
         language.lanname as function_language,
         procedure.prokind as function_kind,
         procedure.pronargs as function_argument_count,
         procedure.prorettype
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
   join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
    join pg_catalog.pg_language language on language.oid = procedure.prolang
   where namespace.nspname = 'kb_stage'
     and pg_catalog.left(procedure.proname, 9) = E'teleo_v3_'
     and procedure.proname <> E'teleo_v3_acquire_protected_relation_locks'
), actual_v3_catalog_lock_functions as (
  select procedure.oid,
         owner_role.rolname as function_owner,
         procedure.prosecdef as function_security_definer,
         pg_catalog.to_jsonb(procedure.proconfig) as function_config,
         pg_catalog.encode(
           pg_catalog.sha256(pg_catalog.convert_to(procedure.prosrc, 'UTF8')),
           'hex'
         ) as function_source_sha256,
         language.lanname as function_language,
         procedure.prokind as function_kind,
         procedure.pronargs as function_argument_count,
         pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
         procedure.prorettype,
         exists (
           select 1
             from pg_catalog.aclexplode(
               coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
             ) acl
             join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
            where acl.privilege_type = 'EXECUTE'
              and grantee.rolname = 'kb_apply'
         ) as apply_execute,
         not exists (
           select 1
             from pg_catalog.aclexplode(
               coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
             ) acl
            where acl.privilege_type = 'EXECUTE'
              and acl.grantee <> procedure.proowner
              and acl.grantee <> (
                select role.oid from pg_catalog.pg_roles role where role.rolname = 'kb_apply'
              )
         ) as execute_acl_exact
    from pg_catalog.pg_proc procedure
    join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
    join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
    join pg_catalog.pg_language language on language.oid = procedure.prolang
   where namespace.nspname = 'kb_stage'
     and procedure.proname = E'teleo_v3_acquire_protected_relation_locks'
), actual_v3_triggers as (
  select trigger_row.*, table_namespace.nspname as schema_name,
         relation.relname as table_name,
         function_namespace.nspname as function_schema,
         procedure.proname as function_name,
         pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
         coalesce((
           select pg_catalog.jsonb_agg(attribute.attname order by attribute.attname)
             from pg_catalog.unnest(trigger_row.tgattr::smallint[]) update_column(attnum)
             join pg_catalog.pg_attribute attribute
               on attribute.attrelid = trigger_row.tgrelid
              and attribute.attnum = update_column.attnum
              and not attribute.attisdropped
         ), '[]'::jsonb) as update_columns,
         pg_catalog.pg_get_expr(trigger_row.tgqual, trigger_row.tgrelid, true) as when_expression,
         pg_catalog.encode(trigger_row.tgargs, 'hex') as argument_hex
    from pg_catalog.pg_trigger trigger_row
    join pg_catalog.pg_class relation on relation.oid = trigger_row.tgrelid
    join pg_catalog.pg_namespace table_namespace on table_namespace.oid = relation.relnamespace
    join pg_catalog.pg_proc procedure on procedure.oid = trigger_row.tgfoid
    join pg_catalog.pg_namespace function_namespace on function_namespace.oid = procedure.pronamespace
   where pg_catalog.left(trigger_row.tgname, 9) = E'teleo_v3_'
), function_contract as (
  select
    (select pg_catalog.count(*) from actual_v3_functions) =
      (select pg_catalog.count(*) from expected_functions)
    and not exists (
      select 1
        from expected_functions expected
       where not exists (
         select 1
           from actual_v3_functions actual
          where actual.function_schema = expected.function_schema
            and actual.function_name = expected.function_name
            and actual.function_identity_arguments = expected.function_identity_arguments
            and actual.function_owner = expected.function_owner
            and actual.function_security_definer = expected.function_security_definer
            and actual.function_config = expected.function_config
            and actual.function_source_sha256 = expected.function_source_sha256
            and (
              expected.function_owner_only_execute is null
              or actual.function_owner_only_execute = expected.function_owner_only_execute
            )
            and actual.function_language = 'plpgsql'
            and actual.function_kind = 'f'
            and actual.function_argument_count = 0
            and actual.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype
       )
    ) as valid
), catalog_lock_function_contract as (
  select pg_catalog.count(*) = 1
     and pg_catalog.bool_and(
       function_owner = E'kb_gate_owner'
       and function_security_definer
       and function_config = '["search_path=pg_catalog, pg_temp"]'::jsonb
       and function_source_sha256 = E'897345944ba0f1b2a5c01b098fb17de6f881a122e906277086fa7889e76219e8'
       and function_language = 'plpgsql'
       and function_kind = 'f'
       and function_argument_count = 0
       and function_identity_arguments = ''
       and prorettype = 'pg_catalog.void'::pg_catalog.regtype
       and apply_execute
       and execute_acl_exact
     ) as valid
    from actual_v3_catalog_lock_functions
), trigger_contract as (
  select
    (select pg_catalog.count(*) from actual_v3_triggers) =
      (select pg_catalog.count(*) from expected_triggers)
    and not exists (
      select 1
        from expected_triggers expected
       where not exists (
         select 1
           from actual_v3_triggers actual
          where actual.schema_name = expected.schema_name
            and actual.table_name = expected.table_name
            and actual.tgname = expected.trigger_name
            and not actual.tgisinternal
            and actual.tgenabled = 'O'
            and actual.tgtype = expected.trigger_type
            and actual.update_columns = expected.update_columns
            and actual.when_expression is not distinct from expected.when_expression
            and actual.tgnargs = expected.argument_count
            and actual.argument_hex = expected.argument_hex
            and actual.tgconstraint = 0::pg_catalog.oid
            and not actual.tgdeferrable
            and not actual.tginitdeferred
            and actual.tgconstrrelid = 0::pg_catalog.oid
            and actual.tgparentid = 0::pg_catalog.oid
            and actual.tgoldtable is null
            and actual.tgnewtable is null
            and actual.function_schema = expected.function_schema
            and actual.function_name = expected.function_name
            and actual.function_identity_arguments = expected.function_identity_arguments
            and exists (
              select 1
                from actual_v3_functions function_row
               where function_row.oid = actual.tgfoid
                 and function_row.function_schema = expected.function_schema
                 and function_row.function_name = expected.function_name
                 and function_row.function_identity_arguments = expected.function_identity_arguments
            )
       )
    ) as valid
), catalog_state as (
  select
    exists (select 1 from actual_v3_functions)
      or exists (select 1 from actual_v3_catalog_lock_functions)
      or exists (select 1 from actual_v3_triggers) as artifacts_present,
    (select valid from function_contract) as functions_valid,
    (select valid from catalog_lock_function_contract) as catalog_lock_function_valid,
    (select valid from trigger_contract) as triggers_valid,
    exists (
      select 1
        from pg_catalog.pg_roles owner_role
       where owner_role.rolname = E'kb_gate_owner'
         and not owner_role.rolcanlogin
         and not owner_role.rolinherit
         and not owner_role.rolsuper
         and not owner_role.rolcreatedb
         and not owner_role.rolcreaterole
         and not owner_role.rolreplication
         and not owner_role.rolbypassrls
    ) as owner_role_valid,
    not exists (
      select 1
        from pg_catalog.pg_auth_members membership
        join pg_catalog.pg_roles member_role on member_role.oid = membership.member
        join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
       where member_role.rolname in (E'kb_gate_owner', E'kb_review', E'kb_apply')
          or granted_role.rolname in (E'kb_gate_owner', E'kb_review', E'kb_apply')
    ) as memberships_valid
)
select case
  when not artifacts_present then 'V2'
  when functions_valid and catalog_lock_function_valid and triggers_valid
    and owner_role_valid and memberships_valid then 'V3'
  else 'INVALID'
end
  into v_contract_state
from catalog_state;
  -- TELEO_V3_CONTRACT_POSTFLIGHT_QUERY_END
  if v_contract_state <> 'V3' then
    raise exception using
      errcode = '55000',
      message = pg_catalog.format(
        'teleo_v3_epistemic_contract: installed trigger catalog is %s',
        v_contract_state
      );
  end if;
end
$contract_postflight$;

commit;

select jsonb_build_object(
  'contract', 'livingip.teleo-v3-epistemic-contract',
  'version', 1,
  'migration', 'installed',
  'legacy_claims', (select count(*) from public.claims where admission_state = 'legacy'),
  'admitted_claims', (select count(*) from public.claims where admission_state = 'admitted')
)::text;
