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
+10 -16
View File
@@ -1,5 +1,11 @@
import type { Task, TaskStore } from "./types.js"; 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 { export interface ExecutionOrderResult {
/** Tasks in execution order (done first, then pending) */ /** Tasks in execution order (done first, then pending) */
@@ -24,24 +30,13 @@ export async function computeExecutionOrder(
): Promise<ExecutionOrderResult> { ): Promise<ExecutionOrderResult> {
const graph = await loadScopedTaskGraph(store, scopeId); 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( const remaining = new Set(
graph.candidates graph.candidates
.filter((t) => t.status === "open" || t.status === "in_progress") .filter((t) => t.status === "open" || t.status === "in_progress")
.map((t) => t.id), .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 timeline: Task[] = [];
const orderMap = new Map<string, number>(); const orderMap = new Map<string, number>();
let orderIdx = 0; let orderIdx = 0;
@@ -56,9 +51,8 @@ export async function computeExecutionOrder(
// Then pending tasks in execution order // Then pending tasks in execution order
while (remaining.size > 0) { while (remaining.size > 0) {
const readyNow = [...remaining] const readyNow = getTasksById(graph, remaining)
.filter(isReady) .filter((task) => isTaskExecutableInOrder(graph, task.id, simDone))
.map((tid) => graph.taskMap.get(tid)!)
.sort(sortByPriorityThenCreated); .sort(sortByPriorityThenCreated);
if (readyNow.length === 0) break; if (readyNow.length === 0) break;
+65
View File
@@ -6,6 +6,7 @@ export interface TaskGraph {
taskMap: Map<string, Task>; taskMap: Map<string, Task>;
childrenMap: Map<string, Task[]>; childrenMap: Map<string, Task[]>;
candidateIds: Set<string>; candidateIds: Set<string>;
doneTaskIds: Set<string>;
} }
type TaskGraphStore = Pick<TaskStore, "list" | "get" | "getDescendants">; type TaskGraphStore = Pick<TaskStore, "list" | "get" | "getDescendants">;
@@ -35,6 +36,47 @@ export function buildChildrenMap(tasks: Task[]): Map<string, Task[]> {
return childrenMap; 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( export async function loadScopedTaskGraph(
store: TaskGraphStore, store: TaskGraphStore,
scopeId?: string, scopeId?: string,
@@ -48,9 +90,32 @@ export async function loadScopedTaskGraph(
taskMap: buildTaskMap(allTasks), taskMap: buildTaskMap(allTasks),
childrenMap: buildChildrenMap(allTasks), childrenMap: buildChildrenMap(allTasks),
candidateIds: new Set(candidates.map((task) => task.id)), 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( async function loadScopedCandidates(
store: TaskGraphStore, store: TaskGraphStore,
allTasks: Task[], allTasks: Task[],
+10 -25
View File
@@ -2,7 +2,12 @@ import { readdir, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import type { Task, TaskStore, CreateTaskOptions, TaskStatus } from "./types.js"; 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 { function generateId(): string {
return randomBytes(4).toString("hex"); return randomBytes(4).toString("hex");
@@ -253,34 +258,14 @@ export class FileTaskStore implements TaskStore {
async getReady(scopeId?: string): Promise<Task[]> { async getReady(scopeId?: string): Promise<Task[]> {
const graph = await loadScopedTaskGraph(this, scopeId); const graph = await loadScopedTaskGraph(this, scopeId);
return graph.candidates
const isReady = (task: Task): boolean => { .filter((task) => isReadyTask(graph, task))
if (task.status !== "open") return false; .sort(sortByPriorityThenCreated);
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);
} }
async getBlocked(scopeId?: string): Promise<Task[]> { async getBlocked(scopeId?: string): Promise<Task[]> {
const graph = await loadScopedTaskGraph(this, scopeId); const graph = await loadScopedTaskGraph(this, scopeId);
return graph.candidates.filter((task) => isBlockedTask(graph, task));
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);
} }
async getClosed(scopeId?: string): Promise<Task[]> { async getClosed(scopeId?: string): Promise<Task[]> {