import { z } from "zod"; import type { AgentTaskDiscussionContext, AnswerUiSelection } from "@/features/agent"; import { reportUiBlocksSchema } from "@/features/agent"; export type InvestigationDiscussion = { prompt?: string; context?: AgentTaskDiscussionContext; // Display metadata from the selected report, never the task's current round. reportGeneration?: number | null; reference?: AnswerUiSelection; }; const text = z.string().trim().min(1); export const investigationMapTargetSchema = z.object({ source_id: z.enum(["junctions", "pipes", "valves", "pumps", "tanks", "reservoirs", "scada"]), feature_id: text.max(128) }).strict(); export const investigationDiagramSchema = z .object({ title: text, mermaid: text .max(20000) .refine( (source) => /^\s*(?:flowchart|graph)\s+(?:TD|TB|BT|LR|RL)\b/.test(source) && !source.includes("```") && !source.includes("%%{") && !/(?:^|[;\n])\s*click\b/.test(source), "调查图只接受不含指令或回调的 Mermaid 流程描述" ), node_refs: z .array( z .object({ node_id: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,79}$/), label: text, status: z.enum(["pending", "running", "completed", "waiting_human", "failed", "skipped", "cancelled"]).nullish(), map_targets: z.array(investigationMapTargetSchema).max(100).optional(), evidence_refs: z.array(text).default([]), action_refs: z.array(z.string().uuid()).default([]) }) .strict() ) .max(100) .default([]) }) .strict() .superRefine((diagram, ctx) => { if (new Set(diagram.node_refs.map((node) => node.node_id)).size !== diagram.node_refs.length) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "调查图节点引用重复" }); } }); const dataGapSchema = z .object({ gap_id: text, node_id: text.nullish(), missing_data: text, scope: text, impact: text, required_data: text, evidence_refs: z.array(text).min(1), blocking: z.boolean() }) .strict(); /** Historical omissions remain unknown. Never infer outcomes from report prose. */ export const investigationReportSchema = z .object({ title: text, current_conclusion: text, conclusion_boundary: text, evidence_refs: z.array(text), recommendations: z.array(text), pending_items: z.array(text), stage: z.enum(["interim", "final"]).nullish(), outcome: z.enum(["concluded", "insufficient_data"]).nullish(), stop_reason: text.nullish(), data_gaps: z.array(dataGapSchema).nullish(), diagram: investigationDiagramSchema.nullish(), ui_blocks: reportUiBlocksSchema.optional() }) .strict() .superRefine((report, ctx) => { const invalid = (message: string) => ctx.addIssue({ code: z.ZodIssueCode.custom, message }); if (report.outcome == null) { if (report.stage != null || report.stop_reason != null || report.data_gaps != null) invalid("报告结构化字段必须一起提供"); return; } if (report.stage == null || report.data_gaps == null) { invalid("报告缺少阶段或数据缺口清单"); return; } if (new Set(report.data_gaps.map((gap) => gap.gap_id)).size !== report.data_gaps.length) invalid("数据缺口编号重复"); const blocking = report.data_gaps.some((gap) => gap.blocking); if (report.outcome === "insufficient_data") { if (report.stage !== "final" || !report.stop_reason || !blocking) invalid("证据不足必须提供最终报告、结束原因和关键缺口"); if (report.recommendations.length || report.pending_items.length) invalid("证据不足退出后不再生成后续处置建议或待办"); } else if (report.stop_reason != null || blocking) invalid("已形成结论不能包含结束原因或阻塞缺口"); }); export type InvestigationReport = z.infer; export type InvestigationDiagram = z.infer; export type InvestigationMapTarget = z.infer;