refactor(server): centralize task graph readiness (#840)

This commit is contained in:
Mohamed Boudra
2026-05-09 22:06:56 +08:00
committed by GitHub
parent 5fb6e0e74d
commit 0abcd21cd3
3 changed files with 85 additions and 41 deletions

View File

@@ -1,5 +1,11 @@
import type { Task, TaskStore } from "./types.js";
import { buildChildrenMap, loadScopedTaskGraph, sortByPriorityThenCreated } from "./task-graph.js";
import {
buildChildrenMap,
getTasksById,
isTaskExecutableInOrder,
loadScopedTaskGraph,
sortByPriorityThenCreated,
} from "./task-graph.js";
export interface ExecutionOrderResult {
/** Tasks in execution order (done first, then pending) */
@@ -24,24 +30,13 @@ export async function computeExecutionOrder(
): Promise<ExecutionOrderResult> {
const graph = await loadScopedTaskGraph(store, scopeId);
const simDone = new Set(graph.allTasks.filter((t) => t.status === "done").map((t) => t.id));
const simDone = new Set(graph.doneTaskIds);
const remaining = new Set(
graph.candidates
.filter((t) => t.status === "open" || t.status === "in_progress")
.map((t) => t.id),
);
const isReady = (taskId: string): boolean => {
const task = graph.taskMap.get(taskId);
if (!task) return false;
const depsOk = task.deps.every((depId) => simDone.has(depId));
const children = (graph.childrenMap.get(taskId) ?? []).filter((c) =>
graph.candidateIds.has(c.id),
);
const childrenOk = children.every((c) => simDone.has(c.id));
return depsOk && childrenOk;
};
const timeline: Task[] = [];
const orderMap = new Map<string, number>();
let orderIdx = 0;
@@ -56,9 +51,8 @@ export async function computeExecutionOrder(
// Then pending tasks in execution order
while (remaining.size > 0) {
const readyNow = [...remaining]
.filter(isReady)
.map((tid) => graph.taskMap.get(tid)!)
const readyNow = getTasksById(graph, remaining)
.filter((task) => isTaskExecutableInOrder(graph, task.id, simDone))
.sort(sortByPriorityThenCreated);
if (readyNow.length === 0) break;

View File

@@ -6,6 +6,7 @@ export interface TaskGraph {
taskMap: Map<string, Task>;
childrenMap: Map<string, Task[]>;
candidateIds: Set<string>;
doneTaskIds: Set<string>;
}
type TaskGraphStore = Pick<TaskStore, "list" | "get" | "getDescendants">;
@@ -35,6 +36,47 @@ export function buildChildrenMap(tasks: Task[]): Map<string, Task[]> {
return childrenMap;
}
export function isReadyTask(graph: TaskGraph, task: Task): boolean {
return (
task.status === "open" &&
areTaskDepsDone(graph, task, graph.doneTaskIds) &&
areTaskChildrenDone(graph, task.id, graph.doneTaskIds)
);
}
export function isBlockedTask(graph: TaskGraph, task: Task): boolean {
return (
task.status !== "draft" &&
task.status !== "done" &&
task.deps.length > 0 &&
!areTaskDepsDone(graph, task, graph.doneTaskIds)
);
}
export function isTaskExecutableInOrder(
graph: TaskGraph,
taskId: string,
completedTaskIds: Set<string>,
): boolean {
const task = graph.taskMap.get(taskId);
return (
task !== undefined &&
areTaskDepsDone(graph, task, completedTaskIds) &&
areTaskChildrenDone(graph, task.id, completedTaskIds, { scoped: true })
);
}
export function getTasksById(graph: TaskGraph, taskIds: Iterable<string>): Task[] {
const tasks: Task[] = [];
for (const taskId of taskIds) {
const task = graph.taskMap.get(taskId);
if (task) {
tasks.push(task);
}
}
return tasks;
}
export async function loadScopedTaskGraph(
store: TaskGraphStore,
scopeId?: string,
@@ -48,9 +90,32 @@ export async function loadScopedTaskGraph(
taskMap: buildTaskMap(allTasks),
childrenMap: buildChildrenMap(allTasks),
candidateIds: new Set(candidates.map((task) => task.id)),
doneTaskIds: new Set(allTasks.filter((task) => task.status === "done").map((task) => task.id)),
};
}
function areTaskDepsDone(graph: TaskGraph, task: Task, completedTaskIds: Set<string>): boolean {
return task.deps.every((depId) => {
const dep = graph.taskMap.get(depId);
return dep !== undefined && completedTaskIds.has(depId);
});
}
function areTaskChildrenDone(
graph: TaskGraph,
taskId: string,
completedTaskIds: Set<string>,
options?: { scoped: boolean },
): boolean {
const children = graph.childrenMap.get(taskId) ?? [];
return children.every((child) => {
if (options?.scoped === true && !graph.candidateIds.has(child.id)) {
return true;
}
return completedTaskIds.has(child.id);
});
}
async function loadScopedCandidates(
store: TaskGraphStore,
allTasks: Task[],

View File

@@ -2,7 +2,12 @@ import { readdir, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
import { join } from "node:path";
import { randomBytes } from "node:crypto";
import type { Task, TaskStore, CreateTaskOptions, TaskStatus } from "./types.js";
import { loadScopedTaskGraph, sortByPriorityThenCreated } from "./task-graph.js";
import {
isBlockedTask,
isReadyTask,
loadScopedTaskGraph,
sortByPriorityThenCreated,
} from "./task-graph.js";
function generateId(): string {
return randomBytes(4).toString("hex");
@@ -253,34 +258,14 @@ export class FileTaskStore implements TaskStore {
async getReady(scopeId?: string): Promise<Task[]> {
const graph = await loadScopedTaskGraph(this, scopeId);
const isReady = (task: Task): boolean => {
if (task.status !== "open") return false;
const depsReady = task.deps.every((depId) => {
const dep = graph.taskMap.get(depId);
return dep?.status === "done";
});
if (!depsReady) return false;
const children = graph.childrenMap.get(task.id) ?? [];
return children.every((c) => c.status === "done");
};
return graph.candidates.filter(isReady).sort(sortByPriorityThenCreated);
return graph.candidates
.filter((task) => isReadyTask(graph, task))
.sort(sortByPriorityThenCreated);
}
async getBlocked(scopeId?: string): Promise<Task[]> {
const graph = await loadScopedTaskGraph(this, scopeId);
const isBlocked = (task: Task): boolean => {
if (task.status === "draft" || task.status === "done") return false;
if (task.deps.length === 0) return false;
return task.deps.some((depId) => {
const dep = graph.taskMap.get(depId);
return dep?.status !== "done";
});
};
return graph.candidates.filter(isBlocked);
return graph.candidates.filter((task) => isBlockedTask(graph, task));
}
async getClosed(scopeId?: string): Promise<Task[]> {