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]>
This commit is contained in:
1 parent
7ba712ea55
commit
cc8b3a7df4
42 files changed
+7026
-25
No files matched your search
@@ -0,0 +1,123 @@
|
||||
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"]),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user