import type { Kysely } from "kysely"; import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts"; // 规则域:subscription、policy_revision、acquisition_slot、preference_tag、preference_suppression(REQ §6) // 每个 Slot 独立状态;策略版本化(草案/发布/回滚);取消的偏好留抑制记录,Agent 不得据旧证据自动加回。 // biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型 export async function up(db: Kysely): Promise { await execDdl( db, ` ${entityTable( "policy_revision", ` policy_key text not null, revision integer not null check (revision >= 1), kind text not null ${oneOf("kind", ["acquisition", "preference", "retention", "cleanup", "instance", "automation"])}, status text not null default 'draft' ${oneOf("status", ["draft", "published", "superseded", "rolled_back", "archived"])}, title text not null, summary text, scope jsonb not null default '{}'::jsonb, rules jsonb not null default '{}'::jsonb, preview jsonb, based_on_revision_id uuid references policy_revision (id) on delete set null, created_by text, published_at timestamptz, published_by text, constraint policy_revision_key unique (policy_key, revision)`, )} create unique index policy_revision_published_key on policy_revision (policy_key) where status = 'published'; ${entityTable( "subscription", ` media_entity_id uuid references media_entity (id) on delete restrict, season_unit_id uuid references content_unit (id) on delete restrict, title_query text, media_kind text not null ${oneOf("media_kind", ["movie", "series", "variety"])}, mode text not null default 'continuous' ${oneOf("mode", ["once", "continuous"])}, season_scope text not null default 'following_seasons' ${oneOf("season_scope", ["current_season", "following_seasons"])}, origin text not null default 'user' ${oneOf("origin", ["user", "agent", "policy", "import"])}, priority text not null default 'tracking' ${oneOf("priority", ["immediate", "tracking", "new_work", "upgrade"])}, status text not null default 'active' ${oneOf("status", ["active", "paused", "completed", "cancelled"])}, policy_revision_id uuid references policy_revision (id) on delete set null, params jsonb not null default '{}'::jsonb, status_reason text, last_checked_at timestamptz, created_by text, constraint subscription_target_check check (media_entity_id is not null or title_query is not null)`, )} create index subscription_status_idx on subscription (status); create index subscription_entity_idx on subscription (media_entity_id) where media_entity_id is not null; ${entityTable( "acquisition_slot", ` media_entity_id uuid not null references media_entity (id) on delete restrict, content_unit_id uuid references content_unit (id) on delete restrict, subscription_id uuid references subscription (id) on delete set null, policy_revision_id uuid references policy_revision (id) on delete set null, name text not null, status text not null default 'empty' ${oneOf("status", ["empty", "reserved", "downloading", "verifying", "satisfied", "upgrade_pending", "attention"])}, hard_conditions jsonb not null default '{}'::jsonb, soft_preferences jsonb not null default '{}'::jsonb, fallback jsonb not null default '{}'::jsonb, upgrade_threshold jsonb not null default '{}'::jsonb, completion jsonb not null default '{}'::jsonb, distinct_group text, require_distinct_asset boolean not null default false, current_asset_id uuid references media_asset (id) on delete set null, status_reason text, last_evaluated_at timestamptz, next_evaluate_at timestamptz, revision integer not null default 1`, )} create unique index acquisition_slot_target_key on acquisition_slot (media_entity_id, content_unit_id, name) nulls not distinct; create index acquisition_slot_status_idx on acquisition_slot (status); create index acquisition_slot_next_evaluate_idx on acquisition_slot (next_evaluate_at) where next_evaluate_at is not null; create index acquisition_slot_subscription_idx on acquisition_slot (subscription_id) where subscription_id is not null; ${entityTable( "preference_tag", ` label text not null, kind text not null ${oneOf("kind", ["soft", "hard", "version", "target"])}, scope jsonb not null default '{}'::jsonb, rule jsonb not null default '{}'::jsonb, fingerprint text not null, origin text not null default 'manual' ${oneOf("origin", ["manual", "conversation", "agent_inferred"])}, description text, status text not null default 'active' ${oneOf("status", ["active", "disabled", "deleted"])}, revision integer not null default 1, evidence jsonb not null default '{}'::jsonb, policy_revision_id uuid references policy_revision (id) on delete set null`, )} create index preference_tag_status_idx on preference_tag (status); create unique index preference_tag_fingerprint_live_key on preference_tag (fingerprint) where status <> 'deleted'; ${entityTable( "preference_suppression", ` preference_tag_id uuid references preference_tag (id) on delete set null, fingerprint text not null, reason text, evidence jsonb not null default '{}'::jsonb, suppressed_by text not null, active boolean not null default true, lifted_at timestamptz, lifted_by text, constraint preference_suppression_lift_check check (active or lifted_at is not null)`, )} create index preference_suppression_active_idx on preference_suppression (fingerprint) where active; `, ); } // biome-ignore lint/suspicious/noExplicitAny: 同上 export async function down(db: Kysely): Promise { await execDdl( db, dropTables(["preference_suppression", "preference_tag", "acquisition_slot", "subscription", "policy_revision"]), ); }