@@ -8,6 +8,8 @@ export interface TaskClaim {
88
99export interface TaskNode {
1010 id : string ;
11+ title ?: string ;
12+ description ?: string ;
1113 status : TaskStatus ;
1214 dependencies : string [ ] ;
1315 supersedes ?: string [ ] ;
@@ -22,6 +24,36 @@ export interface TaskGraph {
2224 tasks : TaskNode [ ] ;
2325}
2426
27+ export interface RuntimeTask extends TaskNode {
28+ title : string ;
29+ description : string ;
30+ }
31+
32+ export type TaskReadinessStatus = "ready" | "waiting" | "blocked" | "not_pending" ;
33+
34+ export interface TaskReadiness {
35+ taskId : string ;
36+ status : TaskReadinessStatus ;
37+ ready : boolean ;
38+ waitingFor : string [ ] ;
39+ blockedBy : string [ ] ;
40+ reasons : string [ ] ;
41+ }
42+
43+ export interface TaskGraphStatusProjection {
44+ total : number ;
45+ ready : string [ ] ;
46+ blocked : string [ ] ;
47+ waiting : string [ ] ;
48+ completed : string [ ] ;
49+ failed : string [ ] ;
50+ readiness : TaskReadiness [ ] ;
51+ }
52+
53+ export type TaskGraphRuntimeEvent =
54+ | { type : "task_graph.created" ; taskIds : string [ ] ; topologicalOrder : string [ ] }
55+ | { type : "task.ready" ; taskId : string ; reason : string } ;
56+
2557export interface PolicyGraphSketch {
2658 id : string ;
2759 purpose : string ;
@@ -145,11 +177,64 @@ export function validateTaskGraph(graph: TaskGraph): TaskGraphValidationIssue[]
145177}
146178
147179export function computeReadyTaskIds ( graph : TaskGraph ) : string [ ] {
180+ return computeTaskReadiness ( graph ) . filter ( ( item ) => item . ready ) . map ( ( item ) => item . taskId ) ;
181+ }
182+
183+ export function computeTaskReadiness ( graph : TaskGraph ) : TaskReadiness [ ] {
184+ assertValidGraph ( graph ) ;
185+ const tasksById = new Map ( graph . tasks . map ( ( task ) => [ task . id , task ] ) ) ;
148186 const completedOrRepaired = getCompletedOrRepairedTaskIds ( graph ) ;
149- const blocked = new Set ( computeBlockedTaskIds ( graph ) ) ;
150- return graph . tasks
151- . filter ( ( task ) => task . status === "pending" && ! blocked . has ( task . id ) && task . dependencies . every ( ( dependency ) => completedOrRepaired . has ( dependency ) ) )
152- . map ( ( task ) => task . id ) ;
187+ const failed = getFailedBlockingTaskIds ( graph ) ;
188+ const failedBlockersByTask = computeFailedBlockersByTask ( graph , failed ) ;
189+ return graph . tasks . map ( ( task ) => {
190+ if ( task . status !== "pending" ) {
191+ return { taskId : task . id , status : "not_pending" , ready : false , waitingFor : [ ] , blockedBy : [ ] , reasons : [ `task is ${ task . status } ` ] } ;
192+ }
193+ const blockedBy = failedBlockersByTask . get ( task . id ) ?? [ ] ;
194+ const waitingFor = task . dependencies . filter ( ( dependency ) => ! completedOrRepaired . has ( dependency ) && ! failed . has ( dependency ) && ( failedBlockersByTask . get ( dependency ) ?? [ ] ) . length === 0 ) ;
195+ if ( blockedBy . length > 0 ) {
196+ return { taskId : task . id , status : "blocked" , ready : false , waitingFor : [ ] , blockedBy, reasons : blockedBy . map ( ( dependency ) => describeFailedBlocker ( task , dependency , tasksById , failed ) ) } ;
197+ }
198+ if ( waitingFor . length > 0 ) return { taskId : task . id , status : "waiting" , ready : false , waitingFor, blockedBy, reasons : waitingFor . map ( ( dependency ) => `waiting for dependency ${ dependency } ` ) } ;
199+ return { taskId : task . id , status : "ready" , ready : true , waitingFor, blockedBy, reasons : [ task . dependencies . length === 0 ? "no dependencies" : "all dependencies completed" ] } ;
200+ } ) ;
201+ }
202+
203+ export function projectTaskGraphStatus ( graph : TaskGraph ) : TaskGraphStatusProjection {
204+ const readiness = computeTaskReadiness ( graph ) ;
205+ const readinessById = new Map ( readiness . map ( ( item ) => [ item . taskId , item ] ) ) ;
206+ return {
207+ total : graph . tasks . length ,
208+ ready : readiness . filter ( ( item ) => item . ready ) . map ( ( item ) => item . taskId ) ,
209+ blocked : readiness . filter ( ( item ) => item . status === "blocked" ) . map ( ( item ) => item . taskId ) ,
210+ waiting : readiness . filter ( ( item ) => item . status === "waiting" ) . map ( ( item ) => item . taskId ) ,
211+ completed : graph . tasks . filter ( ( task ) => task . status === "completed" ) . map ( ( task ) => task . id ) ,
212+ failed : graph . tasks . filter ( ( task ) => task . status === "failed" && readinessById . get ( task . id ) ?. status !== "blocked" ) . map ( ( task ) => task . id ) ,
213+ readiness,
214+ } ;
215+ }
216+
217+ export function topologicalTaskIds ( graph : TaskGraph ) : string [ ] {
218+ assertValidGraph ( graph ) ;
219+ const remaining = new Map ( graph . tasks . map ( ( task ) => [ task . id , new Set ( task . dependencies ) ] ) ) ;
220+ const ordered : string [ ] = [ ] ;
221+ while ( remaining . size > 0 ) {
222+ const next = Array . from ( remaining . entries ( ) ) . filter ( ( [ , dependencies ] ) => dependencies . size === 0 ) . map ( ( [ taskId ] ) => taskId ) . sort ( ) ;
223+ if ( next . length === 0 ) throw new Error ( "invalid task graph: dependency_cycle" ) ;
224+ for ( const taskId of next ) {
225+ ordered . push ( taskId ) ;
226+ remaining . delete ( taskId ) ;
227+ for ( const dependencies of Array . from ( remaining . values ( ) ) ) dependencies . delete ( taskId ) ;
228+ }
229+ }
230+ return ordered ;
231+ }
232+
233+ export function createTaskGraphRuntimeEvents ( graph : TaskGraph ) : TaskGraphRuntimeEvent [ ] {
234+ return [
235+ { type : "task_graph.created" , taskIds : graph . tasks . map ( ( task ) => task . id ) , topologicalOrder : topologicalTaskIds ( graph ) } ,
236+ ...computeTaskReadiness ( graph ) . filter ( ( item ) => item . ready ) . map ( ( item ) => ( { type : "task.ready" as const , taskId : item . taskId , reason : item . reasons . join ( "; " ) } ) ) ,
237+ ] ;
153238}
154239
155240export function transitionReadyTasks ( graph : TaskGraph ) : TaskGraph {
@@ -159,9 +244,7 @@ export function transitionReadyTasks(graph: TaskGraph): TaskGraph {
159244}
160245
161246export function computeBlockedTaskIds ( graph : TaskGraph ) : string [ ] {
162- const completedOrRepaired = getCompletedOrRepairedTaskIds ( graph ) ;
163- const failed = new Set ( graph . tasks . filter ( ( task ) => task . status === "failed" && ! completedOrRepaired . has ( task . id ) ) . map ( ( task ) => task . id ) ) ;
164- return graph . tasks . filter ( ( task ) => task . status === "pending" && task . dependencies . some ( ( dependency ) => failed . has ( dependency ) ) ) . map ( ( task ) => task . id ) ;
247+ return computeTaskReadiness ( graph ) . filter ( ( item ) => item . status === "blocked" ) . map ( ( item ) => item . taskId ) ;
165248}
166249
167250export function claimTask ( graph : TaskGraph , request : { taskId : string ; workerId : string ; token : string ; now : string ; leaseExpiresAt : string } ) : TaskGraph {
@@ -211,6 +294,45 @@ function assertValidGraph(graph: TaskGraph): void {
211294 if ( issues . length > 0 ) throw new Error ( `invalid task graph: ${ issues . map ( ( issue ) => issue . code ) . join ( ", " ) } ` ) ;
212295}
213296
297+ function computeFailedBlockersByTask ( graph : TaskGraph , failed : Set < string > ) : Map < string , string [ ] > {
298+ const tasksById = new Map ( graph . tasks . map ( ( task ) => [ task . id , task ] ) ) ;
299+ const memo = new Map < string , string [ ] > ( ) ;
300+ const collect = ( taskId : string ) : string [ ] => {
301+ const cached = memo . get ( taskId ) ;
302+ if ( cached ) return cached ;
303+ const task = tasksById . get ( taskId ) ;
304+ if ( ! task ) return [ ] ;
305+ const blockers = new Set < string > ( ) ;
306+ for ( const dependency of task . dependencies ) {
307+ if ( failed . has ( dependency ) ) blockers . add ( dependency ) ;
308+ for ( const upstream of collect ( dependency ) ) blockers . add ( upstream ) ;
309+ }
310+ const result = Array . from ( blockers ) . sort ( ) ;
311+ memo . set ( taskId , result ) ;
312+ return result ;
313+ } ;
314+ for ( const task of graph . tasks ) collect ( task . id ) ;
315+ return memo ;
316+ }
317+
318+ function describeFailedBlocker ( task : TaskNode , failedTaskId : string , tasksById : Map < string , TaskNode > , failed : Set < string > ) : string {
319+ if ( task . dependencies . includes ( failedTaskId ) && failed . has ( failedTaskId ) ) return `dependency ${ failedTaskId } failed` ;
320+ const via = task . dependencies . find ( ( dependency ) => dependsOnFailedTask ( dependency , failedTaskId , tasksById ) ) ;
321+ return via ? `blocked by failed upstream dependency ${ failedTaskId } via ${ via } ` : `blocked by failed upstream dependency ${ failedTaskId } ` ;
322+ }
323+
324+ function dependsOnFailedTask ( taskId : string , failedTaskId : string , tasksById : Map < string , TaskNode > ) : boolean {
325+ const task = tasksById . get ( taskId ) ;
326+ if ( ! task ) return false ;
327+ if ( task . dependencies . includes ( failedTaskId ) ) return true ;
328+ return task . dependencies . some ( ( dependency ) => dependsOnFailedTask ( dependency , failedTaskId , tasksById ) ) ;
329+ }
330+
331+ function getFailedBlockingTaskIds ( graph : TaskGraph ) : Set < string > {
332+ const completedOrRepaired = getCompletedOrRepairedTaskIds ( graph ) ;
333+ return new Set ( graph . tasks . filter ( ( task ) => task . status === "failed" && ! completedOrRepaired . has ( task . id ) ) . map ( ( task ) => task . id ) ) ;
334+ }
335+
214336function getCompletedOrRepairedTaskIds ( graph : TaskGraph ) : Set < string > {
215337 const completedOrRepaired = new Set ( graph . tasks . filter ( ( task ) => task . status === "completed" ) . map ( ( task ) => task . id ) ) ;
216338 for ( const task of graph . tasks ) {
0 commit comments