export interface PlannedPath { src: string; dst: string; } export interface ExistingPath { path: string; size?: number; sha256?: string; } export interface PathConflict { dst: string; kind: "duplicate_in_plan" | "exists" | "case_collision"; srcs: string[]; existing?: ExistingPath; } /** * 冲突检测:计划内多个源写同一目标、目标已存在、(大小写不敏感文件系统上)仅大小写不同。 * 只输出结构,是否覆盖由整理策略与授权决定。 */ export function detectPathConflicts( planned: readonly PlannedPath[], existing: readonly ExistingPath[], opts: { caseInsensitive?: boolean } = {}, ): PathConflict[] { const out: PathConflict[] = []; const byDst = new Map(); for (const p of planned) byDst.set(p.dst, [...(byDst.get(p.dst) ?? []), p.src]); for (const [dst, srcs] of byDst) if (srcs.length > 1) out.push({ dst, kind: "duplicate_in_plan", srcs }); const exact = new Map(existing.map((e) => [e.path, e])); const folded = new Map(existing.map((e) => [e.path.toLowerCase(), e])); for (const [dst, srcs] of byDst) { const hit = exact.get(dst); if (hit) { out.push({ dst, kind: "exists", srcs, existing: hit }); continue; } if (opts.caseInsensitive) { const f = folded.get(dst.toLowerCase()); if (f) out.push({ dst, kind: "case_collision", srcs, existing: f }); } } if (opts.caseInsensitive) { const seen = new Map(); for (const dst of byDst.keys()) { const k = dst.toLowerCase(); const prev = seen.get(k); if (prev !== undefined && prev !== dst) out.push({ dst, kind: "case_collision", srcs: [...(byDst.get(prev) ?? []), ...(byDst.get(dst) ?? [])] }); seen.set(k, dst); } } return out; }