// SPEC: PJ2026-010404 project management draft-2026-06-25-p0-mdtodo-web-active-editing-hwpod-source. // 职责:根据节点自身及全部后代推导只用于展示的 TaskTree 父级时间范围。 export type TaskTreeTimedNode = { id: string; parentId: string | null; startAt: string | null; dueAt: string | null; }; export type TaskTreeTimeRange = Pick; export function deriveEffectiveTaskTimeRanges(tasks: TaskTreeTimedNode[]): Map { const tasksById = new Map(tasks.map((task) => [task.id, task])); const childrenByParent = new Map(); for (const task of tasks) { if (!task.parentId || !tasksById.has(task.parentId)) continue; childrenByParent.set(task.parentId, [...(childrenByParent.get(task.parentId) ?? []), task]); } const ranges = new Map(); const resolving = new Set(); const resolve = (task: TaskTreeTimedNode): TaskTreeTimeRange => { const cached = ranges.get(task.id); if (cached) return cached; if (resolving.has(task.id)) return { startAt: task.startAt, dueAt: task.dueAt }; resolving.add(task.id); const values = [task.startAt, task.dueAt]; for (const child of childrenByParent.get(task.id) ?? []) { const childRange = resolve(child); values.push(childRange.startAt, childRange.dueAt); } const timestamps = values .filter((value): value is string => Boolean(value)) .map((value) => new Date(value).getTime()) .filter(Number.isFinite); const range = timestamps.length > 0 ? { startAt: new Date(Math.min(...timestamps)).toISOString(), dueAt: new Date(Math.max(...timestamps)).toISOString() } : { startAt: null, dueAt: null }; resolving.delete(task.id); ranges.set(task.id, range); return range; }; for (const task of tasks) resolve(task); return ranges; }