// 审稿历史归一:把后端 ReviewHistoryItem 的松散 conflicts(dict[])收紧成 ReviewConflict[]。 // 纯逻辑(可单测);schema 把 conflicts 标成 {[k]:unknown}[],这里安全收窄。 import type { ReviewHistoryItem } from "@/lib/api/types"; import { asCharacterizationIssues } from "./sse"; import type { CharacterizationReport, ForeshadowKind, ForeshadowSuggestion, PaceIssue, PaceReport, ReviewConflict, } from "./sse"; function asString(v: unknown, fallback = ""): string { return typeof v === "string" ? v : fallback; } function asOptionalString(v: unknown): string | null { return typeof v === "string" ? v : null; } function asIntArray(v: unknown): number[] { if (!Array.isArray(v)) return []; return v.filter((x): x is number => typeof x === "number" && Number.isFinite(x)); } // 把一条留痕的 conflicts(ReviewConflictView[],已强类型)映射成 ReviewConflict。 // 保持顺序=下标身份(对齐冲突 gate);type 空串回退「未分类」,refs 容忍缺省。 export function normalizeConflicts( item: ReviewHistoryItem | undefined, ): ReviewConflict[] { const raw = item?.conflicts ?? []; return raw.map((c) => ({ type: c.type || "未分类", where: c.where, refs: c.refs ?? [], suggestion: c.suggestion, // 一键采纳补丁对:留痕里带则透传,供「采纳改法」find-replace 进终稿(缺省 null)。 original: c.original ?? null, replacement: c.replacement ?? null, })); } // 把一条留痕的 foreshadow_sug(扁平 list,每条带 kind)收紧成建议(缺字段给安全默认)。 // 对齐 collect 列映射(C4 扩 T3.3):planted/resolved 扁平成单 list、每条加 kind。 export function normalizeForeshadowSug( item: ReviewHistoryItem | undefined, ): ForeshadowSuggestion[] { const raw = item?.foreshadow_sug ?? []; return raw.map((s) => ({ kind: asKind(s["kind"]), code: asOptionalString(s["code"]), title: asString(s["title"], "(未命名线索)"), where: asOptionalString(s["where"]), note: asOptionalString(s["note"]), })); } function asKind(v: unknown): ForeshadowKind { return v === "resolved" ? "resolved" : "planted"; } // 把一条留痕的 pace(dict)收紧成节奏报告;缺失/非 dict → null(不渲染节拍图)。 export function normalizePace( item: ReviewHistoryItem | undefined, ): PaceReport | null { const raw = item?.pace; if (typeof raw !== "object" || raw === null) return null; const dict = raw as Record; const waterRaw = Array.isArray(dict["water"]) ? dict["water"] : []; const water: PaceIssue[] = waterRaw .filter((w): w is Record => typeof w === "object" && w !== null) .map((w) => ({ where: asString(w["where"]), reason: asString(w["reason"]), })); return { water, hook: dict["hook"] === true, beat_map: asIntArray(dict["beat_map"]), }; } // 把一条留痕的 characterization(dict)收紧成人物塑造报告;缺失/非 dict → null(不渲染)。 // advisory:只展示、无裁决 UI(灵感 D2)。 export function normalizeCharacterization( item: ReviewHistoryItem | undefined, ): CharacterizationReport | null { const raw = item?.characterization; if (typeof raw !== "object" || raw === null) return null; const dict = raw as Record; return { issues: asCharacterizationIssues(dict["issues"]) }; } // 最近一条留痕(GET .../reviews 已按新→旧排序)。 export function latestReview( items: readonly ReviewHistoryItem[] | undefined, ): ReviewHistoryItem | undefined { return items?.[0]; }