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,292 @@
|
||||
import { z } from "zod";
|
||||
import { EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common";
|
||||
import { AcquisitionPriority } from "./policy";
|
||||
|
||||
// 执行域:业务任务、下载计划、下载任务、操作账本(REQ §8.4、§11;ADR-002)
|
||||
|
||||
/** 持久任务类型(任务书 §3.1-8)+ Agent 有界任务 */
|
||||
export const TaskKind = z.enum([
|
||||
"ingestFeed",
|
||||
"resolveObservation",
|
||||
"evaluateAcquisition",
|
||||
"submitDownload",
|
||||
"verifyAndImport",
|
||||
"refreshMetadata",
|
||||
"reconcileLibrary",
|
||||
"cleanupDownloadCache",
|
||||
"resolveDecision",
|
||||
"agentRun",
|
||||
]);
|
||||
export type TaskKind = z.infer<typeof TaskKind>;
|
||||
export const TaskStatus = z.enum(["pending", "running", "waiting", "succeeded", "failed", "cancelled", "dead"]);
|
||||
export type TaskStatus = z.infer<typeof TaskStatus>;
|
||||
|
||||
export const Task = EntityBase.extend({
|
||||
kind: TaskKind,
|
||||
handlerVersion: z.number().int().min(1),
|
||||
idempotencyKey: z.string().min(1),
|
||||
status: TaskStatus,
|
||||
/** 数值越小越先执行 */
|
||||
priority: z.number().int(),
|
||||
input: JsonObject,
|
||||
output: JsonObject.nullable(),
|
||||
subjectType: z.string().nullable(),
|
||||
subjectId: Id.nullable(),
|
||||
parentTaskId: Id.nullable(),
|
||||
engineRunId: z.string().nullable(),
|
||||
attempts: NonNegInt,
|
||||
maxAttempts: z.number().int().min(1),
|
||||
nextRunAt: Timestamp,
|
||||
waitReason: z.string().nullable(),
|
||||
cancelRequestedAt: Timestamp.nullable(),
|
||||
cancelReason: z.string().nullable(),
|
||||
lastError: JsonObject.nullable(),
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
triggeredBy: z.string().nullable(),
|
||||
});
|
||||
export type Task = z.infer<typeof Task>;
|
||||
/** 内部创建输入(幂等键由服务端按语义生成,不能由模型挑选) */
|
||||
export const TaskCreate = Task.pick({
|
||||
kind: true,
|
||||
handlerVersion: true,
|
||||
idempotencyKey: true,
|
||||
priority: true,
|
||||
input: true,
|
||||
subjectType: true,
|
||||
subjectId: true,
|
||||
parentTaskId: true,
|
||||
maxAttempts: true,
|
||||
nextRunAt: true,
|
||||
triggeredBy: true,
|
||||
}).partial({
|
||||
handlerVersion: true,
|
||||
priority: true,
|
||||
input: true,
|
||||
subjectType: true,
|
||||
subjectId: true,
|
||||
parentTaskId: true,
|
||||
maxAttempts: true,
|
||||
nextRunAt: true,
|
||||
triggeredBy: true,
|
||||
});
|
||||
export type TaskCreate = z.infer<typeof TaskCreate>;
|
||||
export const TaskUpdate = Task.pick({
|
||||
status: true,
|
||||
output: true,
|
||||
engineRunId: true,
|
||||
attempts: true,
|
||||
nextRunAt: true,
|
||||
waitReason: true,
|
||||
lastError: true,
|
||||
startedAt: true,
|
||||
finishedAt: true,
|
||||
}).partial();
|
||||
export type TaskUpdate = z.infer<typeof TaskUpdate>;
|
||||
/** 取消 = 停止未来步骤,不撤销已完成外部动作(REQ §11.4) */
|
||||
export const TaskCancelRequest = z.object({ reason: z.string().min(1).max(500) });
|
||||
export type TaskCancelRequest = z.infer<typeof TaskCancelRequest>;
|
||||
|
||||
export const PlanPurpose = z.enum(["fill", "upgrade", "replace_source"]);
|
||||
export type PlanPurpose = z.infer<typeof PlanPurpose>;
|
||||
export const PlanStatus = z.enum(["draft", "reserved", "submitted", "completed", "cancelled", "failed", "expired"]);
|
||||
export type PlanStatus = z.infer<typeof PlanStatus>;
|
||||
|
||||
/** 下载计划:与 Slot 预留、预算预留同事务创建;一个 Slot 同时只能有一个 reserved/submitted 计划 */
|
||||
export const AcquisitionPlan = EntityBase.extend({
|
||||
slotId: Id,
|
||||
sourceReleaseId: Id,
|
||||
taskId: Id.nullable(),
|
||||
purpose: PlanPurpose,
|
||||
status: PlanStatus,
|
||||
priority: AcquisitionPriority,
|
||||
/** evaluateAcquisition 的确定性评估结果与拒绝/接受理由 */
|
||||
evaluation: JsonObject,
|
||||
reservedBytes: NonNegInt,
|
||||
budgetReservation: JsonObject,
|
||||
payloadHash: z.string().min(1),
|
||||
expectedRevision: z.number().int().min(1),
|
||||
idempotencyKey: z.string().min(1),
|
||||
expiresAt: Timestamp.nullable(),
|
||||
statusReason: z.string().nullable(),
|
||||
});
|
||||
export type AcquisitionPlan = z.infer<typeof AcquisitionPlan>;
|
||||
export const AcquisitionPlanCreate = AcquisitionPlan.omit({ ...SERVER_MANAGED, status: true }).partial({
|
||||
taskId: true,
|
||||
purpose: true,
|
||||
priority: true,
|
||||
evaluation: true,
|
||||
reservedBytes: true,
|
||||
budgetReservation: true,
|
||||
expectedRevision: true,
|
||||
expiresAt: true,
|
||||
statusReason: true,
|
||||
});
|
||||
export type AcquisitionPlanCreate = z.infer<typeof AcquisitionPlanCreate>;
|
||||
export const AcquisitionPlanUpdate = AcquisitionPlan.pick({ status: true, taskId: true, statusReason: true }).partial();
|
||||
export type AcquisitionPlanUpdate = z.infer<typeof AcquisitionPlanUpdate>;
|
||||
|
||||
export const DownloadOrigin = z.enum(["submitted", "adopted"]);
|
||||
export type DownloadOrigin = z.infer<typeof DownloadOrigin>;
|
||||
export const DownloadStatus = z.enum([
|
||||
"queued",
|
||||
"submitting",
|
||||
"downloading",
|
||||
"paused",
|
||||
"stalled",
|
||||
"completed",
|
||||
"importing",
|
||||
"imported",
|
||||
"failed",
|
||||
"removed",
|
||||
"missing",
|
||||
]);
|
||||
export type DownloadStatus = z.infer<typeof DownloadStatus>;
|
||||
/** 暂停原因:阈值恢复时只恢复 threshold 暂停项,不恢复用户手动暂停(REQ §8.4.1) */
|
||||
export const PauseReason = z.enum(["user", "threshold", "policy", "agent"]);
|
||||
export type PauseReason = z.infer<typeof PauseReason>;
|
||||
export const TorrentHash = z.string().regex(/^[0-9a-f]{40}$|^[0-9a-f]{64}$/);
|
||||
|
||||
export const DownloadTask = EntityBase.extend({
|
||||
instanceId: Id,
|
||||
/** 该下载器实例内的 torrent 标识(v1 hash 或 v2 hash,小写十六进制) */
|
||||
torrentHash: TorrentHash,
|
||||
infohashV1: z.string().nullable(),
|
||||
infohashV2: z.string().nullable(),
|
||||
acquisitionPlanId: Id.nullable(),
|
||||
sourceReleaseId: Id.nullable(),
|
||||
name: z.string(),
|
||||
savePath: z.string().nullable(),
|
||||
contentPath: z.string().nullable(),
|
||||
category: z.string().nullable(),
|
||||
tags: z.array(z.string()),
|
||||
/** 是否在管理标签范围内;标签移除后暂停后续自动动作 */
|
||||
managed: z.boolean(),
|
||||
origin: DownloadOrigin,
|
||||
status: DownloadStatus,
|
||||
pauseReason: PauseReason.nullable(),
|
||||
rawState: z.string().nullable(),
|
||||
progress: z.number().min(0).max(1),
|
||||
sizeBytes: NonNegInt.nullable(),
|
||||
downloadedBytes: NonNegInt,
|
||||
remainingBytes: NonNegInt.nullable(),
|
||||
ratio: z.number().nullable(),
|
||||
seedingTimeSec: NonNegInt.nullable(),
|
||||
completedAt: Timestamp.nullable(),
|
||||
lastProgressAt: Timestamp.nullable(),
|
||||
/** 无进展起点:只记录并汇入待处理,不自动换源(REQ §8.4.2) */
|
||||
noProgressSince: Timestamp.nullable(),
|
||||
lastSyncedAt: Timestamp.nullable(),
|
||||
nextCheckAt: Timestamp.nullable(),
|
||||
removedAt: Timestamp.nullable(),
|
||||
fileList: z.array(z.object({ path: z.string(), size: NonNegInt, progress: z.number().optional() })).nullable(),
|
||||
lastError: JsonObject.nullable(),
|
||||
});
|
||||
export type DownloadTask = z.infer<typeof DownloadTask>;
|
||||
export const DownloadTaskCreate = DownloadTask.pick({
|
||||
instanceId: true,
|
||||
torrentHash: true,
|
||||
infohashV1: true,
|
||||
infohashV2: true,
|
||||
acquisitionPlanId: true,
|
||||
sourceReleaseId: true,
|
||||
name: true,
|
||||
savePath: true,
|
||||
category: true,
|
||||
tags: true,
|
||||
managed: true,
|
||||
origin: true,
|
||||
}).partial({
|
||||
infohashV1: true,
|
||||
infohashV2: true,
|
||||
acquisitionPlanId: true,
|
||||
sourceReleaseId: true,
|
||||
savePath: true,
|
||||
category: true,
|
||||
tags: true,
|
||||
managed: true,
|
||||
origin: true,
|
||||
});
|
||||
export type DownloadTaskCreate = z.infer<typeof DownloadTaskCreate>;
|
||||
export const DownloadTaskUpdate = DownloadTask.omit({
|
||||
...SERVER_MANAGED,
|
||||
instanceId: true,
|
||||
torrentHash: true,
|
||||
}).partial();
|
||||
export type DownloadTaskUpdate = z.infer<typeof DownloadTaskUpdate>;
|
||||
|
||||
export const OperationPhase = z.enum(["decision", "prepare", "transfer", "verify", "commit", "cleanup", "compensate"]);
|
||||
export type OperationPhase = z.infer<typeof OperationPhase>;
|
||||
export const OperationKind = z.enum([
|
||||
"copy",
|
||||
"move",
|
||||
"rename",
|
||||
"mkdir",
|
||||
"hardlink",
|
||||
"delete_source",
|
||||
"delete_target",
|
||||
"verify",
|
||||
"register",
|
||||
"qb_pause",
|
||||
"qb_resume",
|
||||
"qb_remove",
|
||||
"qb_set_tags",
|
||||
"other",
|
||||
]);
|
||||
export type OperationKind = z.infer<typeof OperationKind>;
|
||||
export const OperationStatus = z.enum(["planned", "started", "succeeded", "failed", "skipped", "compensated"]);
|
||||
export type OperationStatus = z.infer<typeof OperationStatus>;
|
||||
|
||||
/** 副作用账本:plan → attempt(started, attemptToken) → result;提交时以 attemptToken 做 fencing */
|
||||
export const AssetOperation = EntityBase.extend({
|
||||
operationId: z.string().min(1),
|
||||
taskId: Id.nullable(),
|
||||
organizationRecordId: Id.nullable(),
|
||||
planFingerprint: z.string().nullable(),
|
||||
ordinal: z.number().int().nonnegative(),
|
||||
phase: OperationPhase,
|
||||
kind: OperationKind,
|
||||
status: OperationStatus,
|
||||
intent: JsonObject,
|
||||
attemptToken: Id.nullable(),
|
||||
attempts: NonNegInt,
|
||||
result: JsonObject.nullable(),
|
||||
error: JsonObject.nullable(),
|
||||
mediaAssetId: Id.nullable(),
|
||||
downloadTaskId: Id.nullable(),
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
});
|
||||
export type AssetOperation = z.infer<typeof AssetOperation>;
|
||||
export const AssetOperationCreate = AssetOperation.pick({
|
||||
operationId: true,
|
||||
taskId: true,
|
||||
organizationRecordId: true,
|
||||
planFingerprint: true,
|
||||
ordinal: true,
|
||||
phase: true,
|
||||
kind: true,
|
||||
intent: true,
|
||||
mediaAssetId: true,
|
||||
downloadTaskId: true,
|
||||
}).partial({
|
||||
taskId: true,
|
||||
organizationRecordId: true,
|
||||
planFingerprint: true,
|
||||
ordinal: true,
|
||||
intent: true,
|
||||
mediaAssetId: true,
|
||||
downloadTaskId: true,
|
||||
});
|
||||
export type AssetOperationCreate = z.infer<typeof AssetOperationCreate>;
|
||||
export const AssetOperationUpdate = AssetOperation.pick({
|
||||
status: true,
|
||||
attemptToken: true,
|
||||
attempts: true,
|
||||
result: true,
|
||||
error: true,
|
||||
mediaAssetId: true,
|
||||
startedAt: true,
|
||||
finishedAt: true,
|
||||
}).partial();
|
||||
export type AssetOperationUpdate = z.infer<typeof AssetOperationUpdate>;
|
||||
Reference in new issue
Block a user