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:
rootandClaude Fable 5.1 committed 2026-09-27 18:11:37 +08:00
1 parent 7ba712ea55
commit cc8b3a7df4
42 files changed
+7026 -25

No files matched your search

+247
View File
@@ -0,0 +1,247 @@
import { z } from "zod";
import { EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common";
// 规则域:订阅、策略版本、收录 Slot、偏好标签与抑制记录(REQ §6)
export const PolicyKind = z.enum(["acquisition", "preference", "retention", "cleanup", "instance", "automation"]);
export type PolicyKind = z.infer<typeof PolicyKind>;
export const PolicyStatus = z.enum(["draft", "published", "superseded", "rolled_back", "archived"]);
export type PolicyStatus = z.infer<typeof PolicyStatus>;
/** 策略版本:草案 → 预览 → 授权发布 → 版本化/回滚;同一 policyKey 只有一个 published */
export const PolicyRevision = EntityBase.extend({
policyKey: z.string().min(1),
revision: z.number().int().min(1),
kind: PolicyKind,
status: PolicyStatus,
title: z.string().min(1),
summary: z.string().nullable(),
scope: JsonObject,
/** 结构化规则数据;不是可执行脚本或工作流 DSL */
rules: JsonObject,
preview: JsonObject.nullable(),
basedOnRevisionId: Id.nullable(),
createdBy: z.string().nullable(),
publishedAt: Timestamp.nullable(),
publishedBy: z.string().nullable(),
});
export type PolicyRevision = z.infer<typeof PolicyRevision>;
/** 新建总是草案;revision 由服务端分配 */
export const PolicyRevisionCreate = PolicyRevision.pick({
policyKey: true,
kind: true,
title: true,
summary: true,
scope: true,
rules: true,
basedOnRevisionId: true,
}).partial({ summary: true, scope: true, basedOnRevisionId: true });
export type PolicyRevisionCreate = z.infer<typeof PolicyRevisionCreate>;
/** 只有草案可改内容;发布/回滚走动作端点 */
export const PolicyRevisionUpdate = PolicyRevisionCreate.omit({ policyKey: true, kind: true }).partial();
export type PolicyRevisionUpdate = z.infer<typeof PolicyRevisionUpdate>;
export const SubscriptionMediaKind = z.enum(["movie", "series", "variety"]);
export type SubscriptionMediaKind = z.infer<typeof SubscriptionMediaKind>;
export const SubscriptionMode = z.enum(["once", "continuous"]);
export type SubscriptionMode = z.infer<typeof SubscriptionMode>;
/** 仅本季 / 持续后续季(REQ §6.5) */
export const SeasonScope = z.enum(["current_season", "following_seasons"]);
export type SeasonScope = z.infer<typeof SeasonScope>;
export const SubscriptionOrigin = z.enum(["user", "agent", "policy", "import"]);
export type SubscriptionOrigin = z.infer<typeof SubscriptionOrigin>;
/** 默认优先级:即时 > 追更 > 新作品 > 升级(REQ §6.8) */
export const AcquisitionPriority = z.enum(["immediate", "tracking", "new_work", "upgrade"]);
export type AcquisitionPriority = z.infer<typeof AcquisitionPriority>;
export const SubscriptionStatus = z.enum(["active", "paused", "completed", "cancelled"]);
export type SubscriptionStatus = z.infer<typeof SubscriptionStatus>;
export const Subscription = EntityBase.extend({
mediaEntityId: Id.nullable(),
seasonUnitId: Id.nullable(),
/** 作品尚未建档时的检索词 */
titleQuery: z.string().nullable(),
mediaKind: SubscriptionMediaKind,
mode: SubscriptionMode,
seasonScope: SeasonScope,
origin: SubscriptionOrigin,
priority: AcquisitionPriority,
status: SubscriptionStatus,
policyRevisionId: Id.nullable(),
params: JsonObject,
statusReason: z.string().nullable(),
lastCheckedAt: Timestamp.nullable(),
createdBy: z.string().nullable(),
});
export type Subscription = z.infer<typeof Subscription>;
export const SubscriptionCreate = Subscription.pick({
mediaEntityId: true,
seasonUnitId: true,
titleQuery: true,
mediaKind: true,
mode: true,
seasonScope: true,
origin: true,
priority: true,
policyRevisionId: true,
params: true,
})
.partial({
mediaEntityId: true,
seasonUnitId: true,
titleQuery: true,
mode: true,
seasonScope: true,
origin: true,
priority: true,
policyRevisionId: true,
params: true,
})
.refine((v) => !!v.mediaEntityId || !!v.titleQuery, { message: "mediaEntityId 与 titleQuery 至少给一个" });
export type SubscriptionCreate = z.infer<typeof SubscriptionCreate>;
export const SubscriptionUpdate = Subscription.pick({
mode: true,
seasonScope: true,
priority: true,
status: true,
policyRevisionId: true,
params: true,
statusReason: true,
}).partial();
export type SubscriptionUpdate = z.infer<typeof SubscriptionUpdate>;
/** Slot 状态(REQ §6.1) */
export const SlotStatus = z.enum([
"empty",
"reserved",
"downloading",
"verifying",
"satisfied",
"upgrade_pending",
"attention",
]);
export type SlotStatus = z.infer<typeof SlotStatus>;
/** 独立收录目标:硬条件 / 软偏好 / 回退 / 升级阈值 / 完成条件 */
export const AcquisitionSlot = EntityBase.extend({
mediaEntityId: Id,
contentUnitId: Id.nullable(),
subscriptionId: Id.nullable(),
policyRevisionId: Id.nullable(),
/** 用户命名,如 archival / streaming */
name: z.string().min(1).max(64),
status: SlotStatus,
hardConditions: JsonObject,
softPreferences: JsonObject,
fallback: JsonObject,
upgradeThreshold: JsonObject,
completion: JsonObject,
/** 同组 Slot 在 requireDistinctAsset=true 时必须由不同物理资产满足(M2 校验) */
distinctGroup: z.string().nullable(),
requireDistinctAsset: z.boolean(),
currentAssetId: Id.nullable(),
reservedPlanId: Id.nullable(),
statusReason: z.string().nullable(),
lastEvaluatedAt: Timestamp.nullable(),
nextEvaluateAt: Timestamp.nullable(),
revision: z.number().int().min(1),
});
export type AcquisitionSlot = z.infer<typeof AcquisitionSlot>;
export const AcquisitionSlotCreate = AcquisitionSlot.pick({
mediaEntityId: true,
contentUnitId: true,
subscriptionId: true,
policyRevisionId: true,
name: true,
hardConditions: true,
softPreferences: true,
fallback: true,
upgradeThreshold: true,
completion: true,
distinctGroup: true,
requireDistinctAsset: true,
}).partial({
contentUnitId: true,
subscriptionId: true,
policyRevisionId: true,
hardConditions: true,
softPreferences: true,
fallback: true,
upgradeThreshold: true,
completion: true,
distinctGroup: true,
requireDistinctAsset: true,
});
export type AcquisitionSlotCreate = z.infer<typeof AcquisitionSlotCreate>;
/** 规则可改;status/currentAsset/reservedPlan 只由状态机推进,不经 Update */
export const AcquisitionSlotUpdate = AcquisitionSlotCreate.omit({
mediaEntityId: true,
contentUnitId: true,
name: true,
})
.partial()
.extend({ expectedRevision: z.number().int().min(1) });
export type AcquisitionSlotUpdate = z.infer<typeof AcquisitionSlotUpdate>;
/** 偏好类型:soft 影响排序;hard 硬排除;version 版本偏好;target 独立目标配置(REQ §6.7) */
export const PreferenceKind = z.enum(["soft", "hard", "version", "target"]);
export type PreferenceKind = z.infer<typeof PreferenceKind>;
export const PreferenceOrigin = z.enum(["manual", "conversation", "agent_inferred"]);
export type PreferenceOrigin = z.infer<typeof PreferenceOrigin>;
export const PreferenceStatus = z.enum(["active", "disabled", "deleted"]);
export type PreferenceStatus = z.infer<typeof PreferenceStatus>;
export const PreferenceTag = EntityBase.extend({
label: z.string().min(1).max(80),
kind: PreferenceKind,
scope: JsonObject,
rule: JsonObject,
/** 语义指纹(kind+scope+rule 规范化哈希),用于抑制匹配 */
fingerprint: z.string().min(1),
origin: PreferenceOrigin,
description: z.string().nullable(),
status: PreferenceStatus,
revision: z.number().int().min(1),
evidence: JsonObject,
policyRevisionId: Id.nullable(),
});
export type PreferenceTag = z.infer<typeof PreferenceTag>;
/** fingerprint 由服务端计算 */
export const PreferenceTagCreate = PreferenceTag.pick({
label: true,
kind: true,
scope: true,
rule: true,
origin: true,
description: true,
evidence: true,
}).partial({ scope: true, rule: true, origin: true, description: true, evidence: true });
export type PreferenceTagCreate = z.infer<typeof PreferenceTagCreate>;
export const PreferenceTagUpdate = PreferenceTagCreate.omit({ origin: true })
.partial()
.extend({ status: PreferenceStatus.optional() });
export type PreferenceTagUpdate = z.infer<typeof PreferenceTagUpdate>;
/** 手动取消标签留下的抑制记录:Agent 不得据同一旧证据自动加回 */
export const PreferenceSuppression = EntityBase.extend({
preferenceTagId: Id.nullable(),
fingerprint: z.string().min(1),
reason: z.string().nullable(),
evidence: JsonObject,
suppressedBy: z.string().min(1),
active: z.boolean(),
liftedAt: Timestamp.nullable(),
liftedBy: z.string().nullable(),
});
export type PreferenceSuppression = z.infer<typeof PreferenceSuppression>;
export const PreferenceSuppressionCreate = PreferenceSuppression.omit({
...SERVER_MANAGED,
active: true,
liftedAt: true,
liftedBy: true,
suppressedBy: true,
}).partial({ preferenceTagId: true, reason: true, evidence: true });
export type PreferenceSuppressionCreate = z.infer<typeof PreferenceSuppressionCreate>;
/** 唯一允许的更新是解除抑制 */
export const PreferenceSuppressionUpdate = z.object({ active: z.literal(false), reason: z.string().optional() });
export type PreferenceSuppressionUpdate = z.infer<typeof PreferenceSuppressionUpdate>;