// R2 · 冲突分组/排序 + 跳到下一条未裁决(UX §6.4)。 // 分组仅按 type;严重度排序:已知五类给定优先级,未知殿后。 // 关键:分组/排序只改「显示顺序」,每条仍携原始 index——裁决草稿与 accept 的 // conflict_index 一律以原始下标为准(不变量:conflict_index 覆盖 range(len(conflicts)))。 // 纯逻辑,便于 node 环境单测。 import type { DecisionDraft } from "./decisions"; import type { ReviewConflict } from "./sse"; // 严重度优先级(数字越小越靠前)。对齐 C6 五类 ConflictType。 const SEVERITY_RANK: Record = { 设定违例: 0, 能力不符: 1, 时间线倒错: 2, 地理矛盾: 3, 性格漂移: 4, }; const UNKNOWN_RANK = 99; export interface ConflictItem { conflict: ReviewConflict; index: number; } export interface ConflictGroup { type: string; items: ConflictItem[]; } function severityRank(type: string): number { return SEVERITY_RANK[type] ?? UNKNOWN_RANK; } // 按 type 分组并按严重度排序;组内保留原始出现顺序。 export function groupConflicts( conflicts: readonly ReviewConflict[], ): ConflictGroup[] { const byType = new Map(); conflicts.forEach((conflict, index) => { const items = byType.get(conflict.type) ?? []; items.push({ conflict, index }); byType.set(conflict.type, items); }); return [...byType.entries()] .map(([type, items]) => ({ type, items })) .sort((a, b) => severityRank(a.type) - severityRank(b.type)); } // 显示顺序下的原始下标序列(用于「跳到下一条未裁决」按显示顺序推进)。 export function displayOrder(groups: readonly ConflictGroup[]): number[] { return groups.flatMap((group) => group.items.map((item) => item.index)); } // 显示顺序中、当前聚焦之后的下一条未裁决下标;到末尾环回;无未裁决 → null。 export function nextUnresolvedInOrder( order: readonly number[], drafts: readonly DecisionDraft[], fromIndex: number | null, ): number | null { if (order.length === 0) return null; const pos = fromIndex === null ? -1 : order.indexOf(fromIndex); for (let step = 1; step <= order.length; step++) { const idx = order[(pos + step + order.length) % order.length]; if (idx !== undefined && drafts[idx]?.verdict == null) return idx; } return null; }