1
/** Complete dependency validation for current Team task snapshots. */3
import type { TeamTaskId, TeamTaskSnapshot } from './types.ts'5
/** Task dependency relation rejected by the shared graph validator. */6
export type TeamTaskGraphViolation = 'missing' | 'duplicate' | 'cycle'8
/** Package-private task dependency failure retained for command error mapping. */9
export class TeamTaskGraphError extends Error {10
/**11
* @param message - concrete invalid dependency relation.12
* @param violation - stable relation category used by Team commands.13
*/14
constructor(message: string, readonly violation: TeamTaskGraphViolation) {15
super(message)16
this.name = 'TeamTaskGraphError'17
}18
}20
/**21
* Validate the complete active task graph after replacing one candidate snapshot.22
* @param current - current task snapshots before the candidate event.23
* @param candidate - new or next-revision task snapshot.24
* @throws {TeamTaskGraphError} when an active dependency is missing, duplicated, self-referential, or cyclic.25
*/26
export function assertTaskGraphCandidate(27
current: readonly TeamTaskSnapshot[],28
candidate: TeamTaskSnapshot,29
): void {30
const tasks = new Map(current.map(task => [task.id, task]))31
tasks.set(candidate.id, candidate)33
for (const task of tasks.values()) {34
if (task.status === 'deleted') continue35
const seen = new Set<TeamTaskId>()36
for (const blockerId of task.blockedBy) {37
if (blockerId === task.id) {38
throw new TeamTaskGraphError(`team task "${task.id}" cannot block itself`, 'cycle')39
}40
if (seen.has(blockerId)) {41
throw new TeamTaskGraphError(`team task "${task.id}" repeats blocker "${blockerId}"`, 'duplicate')42
}43
const blocker = tasks.get(blockerId)44
if (blocker === undefined || blocker.status === 'deleted') {45
throw new TeamTaskGraphError(46
`blocker task "${blockerId}" for "${task.id}" is missing or deleted`,47
'missing',48
)49
}50
seen.add(blockerId)51
}52
}54
const visiting = new Set<TeamTaskId>()55
const visited = new Set<TeamTaskId>()56
const visit = (id: TeamTaskId): void => {57
if (visiting.has(id)) {58
throw new TeamTaskGraphError(`task dependency cycle includes "${id}"`, 'cycle')59
}60
if (visited.has(id)) return61
const task = tasks.get(id)62
if (task === undefined || task.status === 'deleted') return63
visiting.add(id)64
for (const blockerId of task.blockedBy) visit(blockerId)65
visiting.delete(id)66
visited.add(id)67
}68
for (const task of tasks.values()) visit(task.id)69
}