Files
UHD_Media_Tool/packages/db/migrations/0008_policy.ts
T
rootandClaude Fable 5.1 cc8b3a7df4 feat(db,contracts): full v0.1 data model migrations and shared zod contracts (W1-1)
- migrations 0002-0012 split by domain (foundation/instances/content/identity/
  sources/assets/policy/execution/agent/ops/organization): 41 tables, uuid pk,
  created_at/updated_at + touch trigger, text+CHECK status sets, pgcrypto/pg_trgm
- key constraints: source_release(site_id, site_release_id), observation(feed_id, guid),
  external_identity accepted partial unique, download_task(instance_id, torrent_hash),
  one active acquisition_plan per slot, one active path_binding per entity/season,
  asset_location root+path uniqueness, organization_record change_seq/change_xid cursor
- secret table (ciphertext/nonce/kdf) + secret_ref columns; outbox dispatch fields (ADR-002)
- src/types.ts generated from the migrated schema (scripts/gen-types.ts), drift-checked in tests;
  int8 -> number, date -> string parsers; migrator accepts migrationTableSchema
- contracts: Row/Create/Update schemas per domain, public organization-record API
  (changeSeq cursor), GlobalSettings, CapabilityDescriptor + 14 capability IO schemas,
  typed SSE events, Api endpoint table
- tests: migration up/down round trip on real PG in an isolated schema, constraint checks,
  contracts<->db column/enum sync, positive/negative parse for every exported schema

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:11:37 +08:00

124 lines
5.7 KiB
TypeScript

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<any>): Promise<void> {
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<any>): Promise<void> {
await execDdl(
db,
dropTables(["preference_suppression", "preference_tag", "acquisition_slot", "subscription", "policy_revision"]),
);
}