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,218 @@
|
||||
import { z } from "zod";
|
||||
import { MediaAsset } from "./assets";
|
||||
import type { KnownCapabilityId } from "./capability";
|
||||
import { Id, JsonObject, NonNegInt } from "./common";
|
||||
import { MediaEntity, MediaKind } from "./content";
|
||||
import { AcquisitionPlan, DownloadTask, Task, TaskStatus } from "./execution";
|
||||
import { ExternalEntityType, ExternalIdentity } from "./identity";
|
||||
import { ServerMapping } from "./ops";
|
||||
import { OrganizationAction, OrganizationRecord } from "./organization";
|
||||
import { AcquisitionSlot, PolicyRevision } from "./policy";
|
||||
import { SourceRelease } from "./sources";
|
||||
|
||||
// 首批能力的输入/输出 schema;名字与 CAPABILITY_CATALOG 中的 inputSchema/outputSchema 一致。
|
||||
// 写能力的输入只接受计划/对象 ID,不接受模型自拟的幂等键或任意路径。
|
||||
|
||||
const Limit = z.number().int().min(1).max(100).default(20);
|
||||
|
||||
/** 写能力的通用受理结果:返回 task/plan ID,由任务页追踪(REQ §12.3) */
|
||||
export const Accepted = z.object({ taskId: Id, planId: Id.nullable() });
|
||||
export type Accepted = z.infer<typeof Accepted>;
|
||||
|
||||
export const CatalogLookupInput = z
|
||||
.object({
|
||||
query: z.string().min(1).max(200).optional(),
|
||||
externalId: z.object({ provider: z.string(), entityType: ExternalEntityType, externalId: z.string() }).optional(),
|
||||
kind: MediaKind.optional(),
|
||||
year: z.number().int().optional(),
|
||||
includeRemote: z.boolean().default(false),
|
||||
limit: Limit,
|
||||
})
|
||||
.refine((v) => !!v.query || !!v.externalId, { message: "query 与 externalId 至少给一个" });
|
||||
export const CatalogLookupOutput = z.object({
|
||||
local: z.array(z.object({ entity: MediaEntity, score: z.number(), matchedAlias: z.string().nullable() })),
|
||||
remote: z.array(
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
entityType: ExternalEntityType,
|
||||
externalId: z.string(),
|
||||
title: z.string(),
|
||||
year: z.number().int().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const ReleaseSearchInput = z.object({
|
||||
query: z.string().min(1).max(200).optional(),
|
||||
mediaEntityId: Id.optional(),
|
||||
siteIds: z.array(Id).optional(),
|
||||
/** 本地快照不足时是否实时补查 */
|
||||
allowLiveSearch: z.boolean().default(true),
|
||||
limit: Limit,
|
||||
});
|
||||
export const ReleaseSearchOutput = z.object({
|
||||
items: z.array(SourceRelease),
|
||||
/** 各站点快照新鲜度与是否实时补查 */
|
||||
coverage: z.array(
|
||||
z.object({ siteId: Id, fromSnapshot: z.boolean(), live: z.boolean(), error: z.string().nullable() }),
|
||||
),
|
||||
});
|
||||
|
||||
export const ReleaseInspectInput = z.object({ sourceReleaseId: Id, refreshDetail: z.boolean().default(false) });
|
||||
export const ReleaseInspectOutput = z.object({ release: SourceRelease, inventory: z.array(MediaAsset) });
|
||||
|
||||
export const IdentityResolveInput = z
|
||||
.object({
|
||||
observationId: Id.optional(),
|
||||
sourceReleaseId: Id.optional(),
|
||||
candidate: z
|
||||
.object({
|
||||
mediaEntityId: Id.optional(),
|
||||
provider: z.string().optional(),
|
||||
entityType: ExternalEntityType.optional(),
|
||||
externalId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
evidenceIds: z.array(Id).default([]),
|
||||
})
|
||||
.refine((v) => !!v.observationId || !!v.sourceReleaseId, { message: "需要 observationId 或 sourceReleaseId" });
|
||||
export const IdentityResolveOutput = z.object({
|
||||
status: z.enum(["resolved", "proposed", "ambiguous", "unidentifiable"]),
|
||||
mediaEntityId: Id.nullable(),
|
||||
identities: z.array(ExternalIdentity),
|
||||
decisionId: Id.nullable(),
|
||||
});
|
||||
|
||||
export const DownloadPlanInput = z.object({ slotId: Id, sourceReleaseId: Id });
|
||||
export const DownloadPlanOutput = z.object({
|
||||
plan: AcquisitionPlan.nullable(),
|
||||
slot: AcquisitionSlot,
|
||||
/** 硬条件不满足或预算/阈值阻止时的原因 */
|
||||
rejected: z.array(z.object({ code: z.string(), message: z.string() })),
|
||||
requiresApproval: z.boolean(),
|
||||
});
|
||||
|
||||
export const DownloadSubmitInput = z.object({ planId: Id, approvalId: Id.optional() });
|
||||
export const DownloadSubmitOutput = Accepted.extend({ downloadTask: DownloadTask.nullable() });
|
||||
|
||||
export const OrganizePlanInput = z
|
||||
.object({
|
||||
downloadTaskId: Id.optional(),
|
||||
mediaAssetIds: z.array(Id).optional(),
|
||||
action: OrganizationAction.default("copy"),
|
||||
targetLibraryRootId: Id.optional(),
|
||||
})
|
||||
.refine((v) => !!v.downloadTaskId || !!v.mediaAssetIds?.length, { message: "需要 downloadTaskId 或 mediaAssetIds" });
|
||||
export const OrganizePlanOutput = z.object({
|
||||
planId: Id,
|
||||
payloadHash: z.string(),
|
||||
files: z.array(
|
||||
z.object({
|
||||
sourcePath: z.string(),
|
||||
targetPath: z.string().nullable(),
|
||||
action: OrganizationAction,
|
||||
skipReason: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
conflicts: z.array(z.object({ code: z.string(), message: z.string(), path: z.string().nullable() })),
|
||||
requiresApproval: z.boolean(),
|
||||
});
|
||||
|
||||
export const OrganizeExecuteInput = z.object({ planId: Id, payloadHash: z.string().min(1), approvalId: Id.optional() });
|
||||
export const OrganizeExecuteOutput = Accepted.extend({ record: OrganizationRecord.nullable() });
|
||||
|
||||
export const TaskInspectInput = z.object({ taskId: Id });
|
||||
export const TaskInspectOutput = z.object({
|
||||
task: Task,
|
||||
children: z.array(Task),
|
||||
timeline: z.array(z.object({ at: z.string(), type: z.string(), summary: z.string(), data: JsonObject.optional() })),
|
||||
});
|
||||
|
||||
export const TaskRetryInput = z.object({ taskId: Id, reason: z.string().max(500).optional() });
|
||||
export const TaskRetryOutput = z.object({ taskId: Id, status: TaskStatus });
|
||||
|
||||
export const TaskCancelInput = z.object({ taskId: Id, reason: z.string().min(1).max(500) });
|
||||
export const TaskCancelOutput = z.object({ taskId: Id, status: TaskStatus, cancelRequested: z.boolean() });
|
||||
|
||||
export const PolicyPreviewInput = z.object({
|
||||
policyRevisionId: Id,
|
||||
sampleLimit: z.number().int().min(1).max(1000).default(200),
|
||||
});
|
||||
export const PolicyPreviewOutput = z.object({
|
||||
newlyMatched: NonNegInt,
|
||||
newlyExcluded: NonNegInt,
|
||||
samples: z.array(z.object({ sourceReleaseId: Id, before: z.string(), after: z.string(), reason: z.string() })),
|
||||
});
|
||||
|
||||
export const PolicyApplyInput = z.object({
|
||||
policyRevisionId: Id,
|
||||
expectedRevision: z.number().int().min(1),
|
||||
approvalId: Id.optional(),
|
||||
});
|
||||
export const PolicyApplyOutput = z.object({ policy: PolicyRevision });
|
||||
|
||||
export const InventoryQueryInput = z.object({
|
||||
mediaEntityId: Id.optional(),
|
||||
query: z.string().max(200).optional(),
|
||||
includeServerInventory: z.boolean().default(false),
|
||||
limit: Limit,
|
||||
});
|
||||
export const InventoryQueryOutput = z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
entity: MediaEntity,
|
||||
assets: z.array(MediaAsset),
|
||||
slots: z.array(AcquisitionSlot),
|
||||
identities: z.array(ExternalIdentity.pick({ provider: true, entityType: true, externalId: true, status: true })),
|
||||
/** 外部库存证据;"未知 ≠ 缺失" */
|
||||
server: z.array(ServerMapping),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/** 能力 ID → 输入/输出 schema(server 注册处理器、agent 生成工具定义时共用) */
|
||||
export const CAPABILITY_IO = {
|
||||
"catalog.lookup": { input: CatalogLookupInput, output: CatalogLookupOutput },
|
||||
"release.search": { input: ReleaseSearchInput, output: ReleaseSearchOutput },
|
||||
"release.inspect": { input: ReleaseInspectInput, output: ReleaseInspectOutput },
|
||||
"identity.resolve": { input: IdentityResolveInput, output: IdentityResolveOutput },
|
||||
"download.plan": { input: DownloadPlanInput, output: DownloadPlanOutput },
|
||||
"download.submit": { input: DownloadSubmitInput, output: DownloadSubmitOutput },
|
||||
"organize.plan": { input: OrganizePlanInput, output: OrganizePlanOutput },
|
||||
"organize.execute": { input: OrganizeExecuteInput, output: OrganizeExecuteOutput },
|
||||
"task.inspect": { input: TaskInspectInput, output: TaskInspectOutput },
|
||||
"task.retry": { input: TaskRetryInput, output: TaskRetryOutput },
|
||||
"task.cancel": { input: TaskCancelInput, output: TaskCancelOutput },
|
||||
"policy.preview": { input: PolicyPreviewInput, output: PolicyPreviewOutput },
|
||||
"policy.apply": { input: PolicyApplyInput, output: PolicyApplyOutput },
|
||||
"inventory.query": { input: InventoryQueryInput, output: InventoryQueryOutput },
|
||||
} as const satisfies Record<KnownCapabilityId, { input: z.ZodType; output: z.ZodType }>;
|
||||
|
||||
export type CatalogLookupInput = z.infer<typeof CatalogLookupInput>;
|
||||
export type CatalogLookupOutput = z.infer<typeof CatalogLookupOutput>;
|
||||
export type ReleaseSearchInput = z.infer<typeof ReleaseSearchInput>;
|
||||
export type ReleaseSearchOutput = z.infer<typeof ReleaseSearchOutput>;
|
||||
export type ReleaseInspectInput = z.infer<typeof ReleaseInspectInput>;
|
||||
export type ReleaseInspectOutput = z.infer<typeof ReleaseInspectOutput>;
|
||||
export type IdentityResolveInput = z.infer<typeof IdentityResolveInput>;
|
||||
export type IdentityResolveOutput = z.infer<typeof IdentityResolveOutput>;
|
||||
export type DownloadPlanInput = z.infer<typeof DownloadPlanInput>;
|
||||
export type DownloadPlanOutput = z.infer<typeof DownloadPlanOutput>;
|
||||
export type DownloadSubmitInput = z.infer<typeof DownloadSubmitInput>;
|
||||
export type DownloadSubmitOutput = z.infer<typeof DownloadSubmitOutput>;
|
||||
export type OrganizePlanInput = z.infer<typeof OrganizePlanInput>;
|
||||
export type OrganizePlanOutput = z.infer<typeof OrganizePlanOutput>;
|
||||
export type OrganizeExecuteInput = z.infer<typeof OrganizeExecuteInput>;
|
||||
export type OrganizeExecuteOutput = z.infer<typeof OrganizeExecuteOutput>;
|
||||
export type TaskInspectInput = z.infer<typeof TaskInspectInput>;
|
||||
export type TaskInspectOutput = z.infer<typeof TaskInspectOutput>;
|
||||
export type TaskRetryInput = z.infer<typeof TaskRetryInput>;
|
||||
export type TaskRetryOutput = z.infer<typeof TaskRetryOutput>;
|
||||
export type TaskCancelInput = z.infer<typeof TaskCancelInput>;
|
||||
export type TaskCancelOutput = z.infer<typeof TaskCancelOutput>;
|
||||
export type PolicyPreviewInput = z.infer<typeof PolicyPreviewInput>;
|
||||
export type PolicyPreviewOutput = z.infer<typeof PolicyPreviewOutput>;
|
||||
export type PolicyApplyInput = z.infer<typeof PolicyApplyInput>;
|
||||
export type PolicyApplyOutput = z.infer<typeof PolicyApplyOutput>;
|
||||
export type InventoryQueryInput = z.infer<typeof InventoryQueryInput>;
|
||||
export type InventoryQueryOutput = z.infer<typeof InventoryQueryOutput>;
|
||||
Reference in new issue
Block a user