@@ -210,6 +210,23 @@ static string Category(Snapshot s, JsonElement wi)
210210 _ => "InProgress" ,
211211 } ;
212212
213+ static bool HasField ( Snapshot s , string type , string reference ) =>
214+ s . FieldsOf ( type ) is { Count : > 0 } f ? f . Any ( x => x . Ref == reference ) : s . Fields . ContainsKey ( reference ) ;
215+
216+ /// <summary>
217+ /// Not started, or in the first in-progress state of its type. Later (in test, waiting for production) a missing
218+ /// estimate or criteria is no longer worth a flag.
219+ /// </summary>
220+ static bool Early ( Snapshot s , JsonElement wi )
221+ {
222+ var category = Category ( s , wi ) ;
223+ if ( category == "Proposed" ) return true ;
224+ if ( category != "InProgress" ) return false ;
225+ var type = s . FindType ( WorkItems . Field ( wi , "System.WorkItemType" ) ?? "" ) ;
226+ var first = type ? . States . FirstOrDefault ( x => x . Category == "InProgress" ) ? . Name ;
227+ return first is null || first . Equals ( WorkItems . Field ( wi , "System.State" ) , StringComparison . OrdinalIgnoreCase ) ;
228+ }
229+
213230 static string Group ( string category ) => category switch
214231 {
215232 "Proposed" => "not started" ,
@@ -287,17 +304,28 @@ public static async Task<string> SprintAsync(Lens lens, string? connection, stri
287304 }
288305 sb . AppendLine ( ) ;
289306
290- var remaining = items . Where ( w => Group ( Category ( s , w ) ) is not ( "done" or "removed" ) ) . Sum ( w => Number ( w , RemainingRef ) ?? 0 ) ;
307+ var open = items . Where ( w => Group ( Category ( s , w ) ) is not ( "done" or "removed" ) ) . ToList ( ) ;
308+ var remaining = open . Sum ( w => Number ( w , RemainingRef ) ?? 0 ) ;
309+ // Remaining Work is kept on tasks; when most open tasks have none, a "fits" would only be a guess.
310+ var openTasks = open . Where ( w => Level ( s , w ) == Levels . Task ) . ToList ( ) ;
311+ var kept = openTasks . Count ( w => Number ( w , RemainingRef ) is not null ) ;
291312 if ( it . Finish is { } end && day <= end && s . Fields . ContainsKey ( RemainingRef ) )
292313 {
293- var cap = await TryAsync ( ( ) => boards . CapacityLeftAsync ( name , t , it , it . Start is { } st && st > day ? st : day , working , ct ) ) ;
294- sb . AppendLine ( cap is { } c
295- ? $ "remaining work { Num ( remaining ) } h, capacity left { Num ( c . Hours ) } h ({ c . People } people) - " +
296- ( remaining <= c . Hours ? $ "fits, { Num ( c . Hours - remaining ) } h spare" : $ "OVER by { Num ( remaining - c . Hours ) } h")
297- : $ "remaining work { Num ( remaining ) } h; no capacity set for the team") ;
314+ if ( openTasks . Count > 0 && kept * 2 < openTasks . Count )
315+ sb . AppendLine ( $ "remaining work is filled on only { kept } of { openTasks . Count } open tasks ({ Num ( remaining ) } h) - too little to compare with capacity") ;
316+ else
317+ {
318+ var cap = await TryAsync ( ( ) => boards . CapacityLeftAsync ( name , t , it , it . Start is { } st && st > day ? st : day , working , ct ) ) ;
319+ sb . AppendLine ( cap is { } c
320+ ? $ "remaining work { Num ( remaining ) } h, capacity left { Num ( c . Hours ) } h ({ c . People } people) - " +
321+ ( remaining <= c . Hours ? $ "fits, { Num ( c . Hours - remaining ) } h spare" : $ "OVER by { Num ( remaining - c . Hours ) } h")
322+ : $ "remaining work { Num ( remaining ) } h; no capacity set for the team") ;
323+ }
298324 }
299325
300326 var attention = new List < string > ( ) ;
327+ var attentionTop = 0 ;
328+ var topIds = live . Select ( WorkItems . Id ) . ToHashSet ( ) ;
301329 foreach ( var w in live . Concat ( children . Values . SelectMany ( x => x ) ) . Where ( w => Group ( Category ( s , w ) ) is not ( "done" or "removed" ) ) )
302330 {
303331 var why = new List < string > ( ) ;
@@ -306,15 +334,23 @@ public static async Task<string> SprintAsync(Lens lens, string? connection, stri
306334 why . Add ( $ "no change for { ( day - ch . ToLocalTime ( ) . Date ) . Days } days") ;
307335 if ( ( WorkItems . Field ( w , "System.Tags" ) ?? "" ) . Split ( ';' ) . Any ( x => x . Trim ( ) . Equals ( "blocked" , StringComparison . OrdinalIgnoreCase ) )
308336 || WorkItems . Field ( w , BlockedRef ) is "Yes" ) why . Add ( "blocked" ) ;
309- if ( level == Levels . Requirement && estimate is not null && Number ( w , estimate ) is null ) why . Add ( "no estimate" ) ;
310- if ( level == Levels . Requirement && fields . Contains ( WorkItems . AcceptanceRef ) && Html . ToMarkdown ( WorkItems . Field ( w , WorkItems . AcceptanceRef ) ) . Length == 0 )
311- why . Add ( "no acceptance criteria" ) ;
337+ var typeName = WorkItems . Field ( w , "System.WorkItemType" ) ?? "" ;
338+ if ( level == Levels . Requirement && Early ( s , w ) )
339+ {
340+ if ( estimate is not null && HasField ( s , typeName , estimate ) && Number ( w , estimate ) is null ) why . Add ( "no estimate" ) ;
341+ if ( fields . Contains ( WorkItems . AcceptanceRef ) && HasField ( s , typeName , WorkItems . AcceptanceRef )
342+ && Html . ToMarkdown ( WorkItems . Field ( w , WorkItems . AcceptanceRef ) ) . Length == 0 )
343+ why . Add ( "no acceptance criteria" ) ;
344+ }
312345 if ( WorkItems . Field ( w , "System.AssignedTo" ) is null ) why . Add ( "unassigned" ) ;
313- if ( why . Count > 0 ) attention . Add ( $ "- { WorkItemFormat . Header ( w ) } { Who ( w ) } : { string . Join ( "; " , why ) } ") ;
346+ if ( why . Count == 0 ) continue ;
347+ attention . Add ( $ "- { WorkItemFormat . Header ( w ) } { Who ( w ) } : { string . Join ( "; " , why ) } ") ;
348+ if ( topIds . Contains ( WorkItems . Id ( w ) ) ) attentionTop ++ ;
314349 }
315350 if ( attention . Count > 0 )
316351 {
317- sb . AppendLine ( $ "attention ({ attention . Count } ):") ;
352+ var taskCount = attention . Count - attentionTop ;
353+ sb . AppendLine ( taskCount > 0 ? $ "attention ({ attention . Count } : { attentionTop } items, { taskCount } of their tasks):" : $ "attention ({ attention . Count } ):") ;
318354 foreach ( var a in attention . Take ( detail == "full" ? 200 : 15 ) ) sb . AppendLine ( a ) ;
319355 if ( attention . Count > 15 && detail != "full" ) sb . AppendLine ( $ "- … { attention . Count - 15 } more (detail=full)") ;
320356 }
0 commit comments