import { z } from "zod"; import { publishPlatformSchema } from "../domain/validation"; import type { CalibrationDirection, CalibrationEvent, PerformanceMetrics, PerformanceSnapshot, PerformanceSource, PublicationRecord, RubricVersion, ScoringRun, } from "./types"; const optionalTextSchema = z .preprocess((value) => (value == null ? "" : value), z.string()) .transform((value) => value.trim()); function optionalMetric(value: unknown) { if (value == null || value === "") return undefined; const numberValue = typeof value === "number" ? value : Number(value); return Number.isFinite(numberValue) && numberValue >= 0 ? numberValue : value; } const metricsShape = { views: z.preprocess(optionalMetric, z.number().nonnegative().optional()), impressions: z.preprocess(optionalMetric, z.number().nonnegative().optional()), clicks: z.preprocess(optionalMetric, z.number().nonnegative().optional()), inquiries: z.preprocess(optionalMetric, z.number().nonnegative().optional()), likes: z.preprocess(optionalMetric, z.number().nonnegative().optional()), comments: z.preprocess(optionalMetric, z.number().nonnegative().optional()), shares: z.preprocess(optionalMetric, z.number().nonnegative().optional()), saves: z.preprocess(optionalMetric, z.number().nonnegative().optional()), average_position: z.preprocess(optionalMetric, z.number().nonnegative().optional()), }; export const performanceMetricsSchema = z .object(metricsShape) .transform( (metrics) => Object.fromEntries( Object.entries(metrics).filter(([, value]) => value !== undefined), ) as PerformanceMetrics, ); export const publicationInputSchema = z.object({ platform: publishPlatformSchema.optional(), publish_target: z.string().trim().min(1).optional(), url: z.string().trim().url(), published_at: z.string().datetime(), notes: optionalTextSchema.default(""), }).transform((input) => ({ platform: input.platform, publish_target: input.publish_target ?? input.platform ?? "未指定", url: input.url, published_at: input.published_at, notes: input.notes, })); export const manualPerformanceInputSchema = z .object({ window_label: z.string().trim().min(1), feedback_summary: optionalTextSchema.default(""), raw_reference: optionalTextSchema.optional(), ...metricsShape, }) .transform(({ window_label, feedback_summary, raw_reference, ...metrics }) => ({ window_label, feedback_summary, raw_reference: raw_reference || undefined, metrics: performanceMetricsSchema.parse(metrics), })); function isPerformanceSource(value: unknown): value is PerformanceSource { return ( value === "manual" || (typeof value === "string" && value.startsWith("adapter:") && value.length > "adapter:".length) ); } function rejectUnsafeRawReference(value: string | undefined) { if (!value) return value; if (/cookie|sessionid|token|secret|api[_-]?key|authorization/i.test(value)) { throw new Error("raw_reference must not contain credentials"); } return value; } export const rubricVersionSchema = z.object({ id: z.string().trim().min(1), version: z.string().trim().min(1), name: z.string().trim().min(1), dimensions: z.array( z.object({ id: z.string().trim().min(1), label: z.string().trim().min(1), weight: z.number().positive(), description: z.string().trim().min(1), }), ), formula: z.literal("weighted_average_0_to_10"), is_active: z.boolean(), created_at: z.string().datetime(), }) satisfies z.ZodType; export const scoringRunSchema = z.object({ id: z.string().trim().min(1), result_version_id: z.string().trim().min(1).nullable().optional(), case_type: z.enum(["article", "human_copy"]).optional(), job_id: z.string().trim().min(1).nullable(), revision: z.number().int().positive().nullable(), rubric_version_id: z.string().trim().min(1), dimension_scores: z.record(z.string(), z.number().min(0).max(5)), composite_score: z.number().min(0).max(10), rationale: z.string().trim(), created_at: z.string().datetime(), }) satisfies z.ZodType; export const publicationRecordSchema = z.object({ id: z.string().trim().min(1), result_version_id: z.string().trim().min(1).nullable(), job_id: z.string().trim().min(1).nullable(), revision: z.number().int().positive().nullable(), publish_target: z.string().trim().min(1), platform: publishPlatformSchema.optional(), url: z.string().trim().url(), published_at: z.string().datetime(), status: z.enum(["draft", "published", "archived"]), notes: z.string().trim(), created_at: z.string().datetime(), updated_at: z.string().datetime(), }) satisfies z.ZodType; export const performanceSnapshotSchema = z .object({ id: z.string().trim().min(1), publication_id: z.string().trim().min(1), source: z.custom(isPerformanceSource), window_label: z.string().trim().min(1), metrics: performanceMetricsSchema, feedback_summary: z.string().trim(), raw_reference: z.string().trim().optional(), snapshot_at: z.string().datetime(), }) .transform((snapshot) => ({ ...snapshot, raw_reference: rejectUnsafeRawReference(snapshot.raw_reference), })) satisfies z.ZodType; export const calibrationDirectionSchema = z.enum([ "better_than_expected", "as_expected", "worse_than_expected", "needs_more_data", ]) satisfies z.ZodType; export const calibrationEventSchema = z.object({ id: z.string().trim().min(1), publication_id: z.string().trim().min(1), scoring_run_id: z.string().trim().min(1), performance_snapshot_id: z.string().trim().min(1), direction: calibrationDirectionSchema, observations: z.array(z.string().trim().min(1)), recommended_action: z.string().trim().min(1), created_at: z.string().datetime(), }) satisfies z.ZodType;