import { isLeadMember } from '@shared/utils/leadDetection'; import { getTeamTaskWorkflowColumn, isTeamTaskTerminalForActionableWork, } from '@shared/utils/teamTaskState'; import { normalizeMemberName, resolveCurrentReviewOwner } from '../../../core/domain'; import type { TeamKanbanManager } from '@main/services/team/TeamKanbanManager'; import type { TeamTaskReader } from '@main/services/team/TeamTaskReader'; import type { TeamTask } from '@shared/types'; export interface MemberWorkSyncTaskImpactResolverDeps { taskReader: Pick; kanbanManager: Pick; activeMemberSource: { loadActiveMemberNames(teamName: string): Promise; }; } export interface MemberWorkSyncTaskImpactResolverResult { memberNames: string[]; fallbackTeamWide: boolean; diagnostics: string[]; } function isDeletedTask(task: Pick): boolean { return task.status === 'deleted' || Boolean(task.deletedAt); } function taskMatchesId(task: TeamTask, taskId: string): boolean { const normalized = taskId.trim().replace(/^#/, ''); return ( task.id === taskId || task.id === normalized || task.displayId === taskId || task.displayId === normalized || task.displayId === `#${normalized}` ); } function taskReferenceKeys(task: Pick): string[] { const keys = [task.id, task.displayId] .map((value) => value?.trim()) .filter((value): value is string => Boolean(value)); return [...new Set(keys.flatMap((value) => [value, value.replace(/^#/, '')]))]; } function findLeadMemberName(activeMembers: string[]): string | null { return activeMembers.find((memberName) => isLeadMember({ name: memberName })) ?? null; } export function extractMemberWorkSyncTaskId(input: { taskId?: string; detail?: string; }): string | null { const explicit = input.taskId?.trim(); if (explicit) { return explicit; } const detail = input.detail?.trim(); if (!detail || detail.startsWith('.') || !detail.endsWith('.json')) { return null; } const fileName = detail.split(/[\\/]/).filter(Boolean).at(-1); const taskId = fileName?.replace(/\.json$/i, '').trim(); return taskId && !taskId.startsWith('.') ? taskId : null; } export class MemberWorkSyncTaskImpactResolver { constructor(private readonly deps: MemberWorkSyncTaskImpactResolverDeps) {} async resolve(input: { teamName: string; taskId: string; }): Promise { const taskId = input.taskId.trim(); if (!taskId) { return { memberNames: [], fallbackTeamWide: true, diagnostics: ['task_id_missing'], }; } const [activeMembers, tasks, kanban] = await Promise.all([ this.deps.activeMemberSource.loadActiveMemberNames(input.teamName), this.deps.taskReader.getTasks(input.teamName), this.deps.kanbanManager.getState(input.teamName), ]); const activeByName = new Map( activeMembers.map((memberName) => [normalizeMemberName(memberName), memberName] as const) ); const impacted = new Set(); const diagnostics: string[] = []; const addDiagnostic = (diagnostic: string): void => { if (!diagnostics.includes(diagnostic)) { diagnostics.push(diagnostic); } }; const addMember = (value: unknown): void => { const normalized = normalizeMemberName(value); const activeName = activeByName.get(normalized); if (activeName) { impacted.add(activeName); } }; const addLead = (): void => { const leadName = findLeadMemberName(activeMembers); if (leadName) { impacted.add(leadName); } else { addDiagnostic('lead_member_unavailable'); } }; const task = tasks.find((candidate) => taskMatchesId(candidate, taskId)); if (!task) { return { memberNames: [], fallbackTeamWide: true, diagnostics: ['task_not_found'], }; } addMember(task.owner); if (!normalizeMemberName(task.owner)) { addLead(); addDiagnostic('task_owner_missing'); } else if (!activeByName.has(normalizeMemberName(task.owner))) { addLead(); addDiagnostic('task_owner_inactive'); } const taskKanbanColumn = kanban.tasks[task.id]?.column; const taskWorkflowColumn = getTeamTaskWorkflowColumn({ ...task, ...(taskKanbanColumn ? { kanbanColumn: taskKanbanColumn } : {}), }); const reviewOwner = taskWorkflowColumn === 'review' ? resolveCurrentReviewOwner({ reviewState: task.reviewState, kanbanReviewer: kanban.tasks[task.id]?.reviewer ?? null, historyEvents: task.historyEvents, }) : null; addMember(reviewOwner?.reviewer); if (taskWorkflowColumn === 'review' && !reviewOwner?.reviewer) { addLead(); addDiagnostic('task_reviewer_missing'); } if (task.needsClarification === 'lead') { addLead(); } const tasksByReference = new Map( tasks.flatMap((candidate) => taskReferenceKeys(candidate).map((key) => [key, candidate] as const) ) ); const brokenDependencies = (task.blockedBy ?? []).filter((dependencyId) => { const dependency = tasksByReference.get(dependencyId); return !dependency || isDeletedTask(dependency); }); if (brokenDependencies.length > 0) { addLead(); addDiagnostic('task_has_broken_dependencies'); } for (const candidate of tasks) { const kanbanColumn = kanban.tasks[candidate.id]?.column; if ( candidate.id === task.id || isTeamTaskTerminalForActionableWork({ ...candidate, ...(kanbanColumn ? { kanbanColumn } : {}), }) ) { continue; } if ( (candidate.blockedBy ?? []).some( (dependencyId) => tasksByReference.get(dependencyId) === task ) ) { addMember(candidate.owner); if (isDeletedTask(task)) { addLead(); addDiagnostic('dependent_task_has_deleted_dependency'); } } } return { memberNames: [...impacted].sort((left, right) => left.localeCompare(right)), fallbackTeamWide: false, diagnostics, }; } }