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,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>;
|
||||
Reference in new issue
Block a user