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

+218
View File
@@ -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>;