@@ -1216,12 +1216,13 @@ export class WorklogDatabase {
12161216 item : WorkItem ,
12171217 now : number ,
12181218 recencyPolicy : 'prefer' | 'avoid' | 'ignore' = 'ignore' ,
1219- ancestorsOfInProgress ?: Set < string >
1219+ ancestorsOfInProgress ?: Set < string > ,
1220+ effectivePriorityCache ?: Map < string , { value : number ; reason : string ; inheritedFrom ?: string } >
12201221 ) : number {
12211222 // Weights are intentionally fixed and not configurable per request
12221223 //
12231224 // Ranking precedence (highest to lowest):
1224- // 1. priority — primary ranking (weight 1000 per level)
1225+ // 1. effective priority — primary ranking (weight 1000 per level)
12251226 // 2. blocksHighPriority — boost for items that unblock high/critical work
12261227 // 3. in-progress multipliers — boost active items and their ancestors
12271228 // 4. blocked penalty — heavy penalty for blocked items
@@ -1237,8 +1238,11 @@ export class WorklogDatabase {
12371238
12381239 let score = 0 ;
12391240
1240- // Priority base
1241- score += this . getPriorityValue ( item . priority ) * WEIGHTS . priority ;
1241+ // Priority base — use effective priority (accounts for inheritance from
1242+ // in-progress parents and blocked dependents) so that score-based tie-breaking
1243+ // is consistent with the effective-priority tiebreaker below.
1244+ const effectivePrio = this . computeEffectivePriority ( item , effectivePriorityCache ) ;
1245+ score += effectivePrio . value * WEIGHTS . priority ;
12421246
12431247 // Blocks-high-priority boost: if this item is a dependency prerequisite for
12441248 // active items with high or critical priority, add a proportional boost.
@@ -1287,8 +1291,22 @@ export class WorklogDatabase {
12871291 }
12881292 }
12891293
1290- // Blocked status - heavy penalty
1291- if ( item . status === 'blocked' ) score += WEIGHTS . blocked ;
1294+ // Blocked status - heavy penalty, but only when the item actually has
1295+ // active dependency edges (i.e., it IS blocked by something active).
1296+ // Items with status='blocked' but no active outgoing edges (e.g., blocked
1297+ // due to a description mention rather than a formal dependency) are
1298+ // treated as normal candidates so their priority can be compared fairly.
1299+ // Items in 'in_review' stage are considered non-blocking when explicitly
1300+ // included (includeInReview) — treat them like open items.
1301+ const hasActiveBlocker = this . store . getDependencyEdgesFrom ( item . id ) . some (
1302+ edge => {
1303+ const target = this . store . getWorkItem ( edge . toId ) ;
1304+ return this . isDependencyActive ( target ?? null ) ;
1305+ }
1306+ ) ;
1307+ if ( item . status === 'blocked' && item . stage !== 'in_review' && hasActiveBlocker ) {
1308+ score += WEIGHTS . blocked ;
1309+ }
12921310
12931311 // In-progress score multiplier boosts (applied after all additive components).
12941312 // Non-stacking: direct in-progress boost takes precedence over ancestor boost.
@@ -1329,6 +1347,49 @@ export class WorklogDatabase {
13291347 } ) ;
13301348 }
13311349
1350+ /**
1351+ * Compute the additive boost component for an item.
1352+ * This is used in selectBySortIndex tie-breaking (separate from the
1353+ * effective-priority base score) to ensure priority (1000) dominates boost (500).
1354+ * Boost = blocks-high-priority component + in-progress ancestor component.
1355+ */
1356+ private computeBoost (
1357+ item : WorkItem ,
1358+ now : number ,
1359+ ancestorsOfInProgress ?: Set < string >
1360+ ) : number {
1361+ const WEIGHTS = {
1362+ blocksHighPriority : 500 ,
1363+ inProgressAncestor : 500 , // maps to ~1.25x multiplier on base score (not directly additive, used here as additive tie-break weight)
1364+ } ;
1365+
1366+ let boost = 0 ;
1367+
1368+ // Blocks-high-priority boost: proportional to max priority of active dependents.
1369+ const inboundEdges = this . store . getDependencyEdgesTo ( item . id ) ;
1370+ let maxBlockedPriorityValue = 0 ;
1371+ for ( const edge of inboundEdges ) {
1372+ const dependent = this . store . getWorkItem ( edge . fromId ) ;
1373+ if ( dependent && dependent . status !== 'completed' && dependent . status !== 'deleted' ) {
1374+ const depPriority = this . getPriorityValue ( dependent . priority ) ;
1375+ if ( depPriority >= 3 && depPriority > maxBlockedPriorityValue ) {
1376+ maxBlockedPriorityValue = depPriority ;
1377+ }
1378+ }
1379+ }
1380+ if ( maxBlockedPriorityValue > 0 ) {
1381+ boost += ( maxBlockedPriorityValue / 3 ) * WEIGHTS . blocksHighPriority ;
1382+ }
1383+
1384+ // In-progress ancestor boost (for tie-breaking only, not a multiplicative score).
1385+ // This ensures items whose ancestors are in-progress get priority in tie-breaking.
1386+ if ( ancestorsOfInProgress ?. has ( item . id ) && item . status !== 'blocked' ) {
1387+ boost += WEIGHTS . inProgressAncestor ;
1388+ }
1389+
1390+ return boost ;
1391+ }
1392+
13321393 private selectBySortIndex (
13331394 items : WorkItem [ ] ,
13341395 effectivePriorityCache ?: Map < string , { value : number ; reason : string ; inheritedFrom ?: string } >
@@ -1341,13 +1402,51 @@ export class WorklogDatabase {
13411402 const allSame = items . every ( item => ( item . sortIndex ?? 0 ) === firstSortIndex ) ;
13421403 if ( allSame ) {
13431404 const cache = effectivePriorityCache ?? new Map ( ) ;
1405+ const now = Date . now ( ) ;
1406+
1407+ // Pre-compute ancestors of in-progress items so that computeScore can
1408+ // apply the parent-in-progress boost during tie-breaking. Use the
1409+ // full store (not just the candidate list) so ancestor detection is
1410+ // accurate across the database.
1411+ const MAX_ANCESTOR_DEPTH = 50 ;
1412+ const ancestorsOfInProgress = new Set < string > ( ) ;
1413+ const allItemsForAncestors = this . store . getAllWorkItems ( ) ;
1414+ for ( const it of allItemsForAncestors ) {
1415+ if ( it . status === 'in-progress' ) {
1416+ let currentParentId = it . parentId ?? null ;
1417+ let depth = 0 ;
1418+ while ( currentParentId && depth < MAX_ANCESTOR_DEPTH ) {
1419+ ancestorsOfInProgress . add ( currentParentId ) ;
1420+ const parent = this . store . getWorkItem ( currentParentId ) ;
1421+ currentParentId = parent ?. parentId ?? null ;
1422+ depth ++ ;
1423+ }
1424+ }
1425+ }
1426+
13441427 const sorted = items . slice ( ) . sort ( ( a , b ) => {
1428+ // 1. Effective priority (descending) — primary ranking factor.
1429+ // Effective priority accounts for inheritance from in-progress parents
1430+ // and blocked dependents.
13451431 const aEffective = this . computeEffectivePriority ( a , cache ) ;
13461432 const bEffective = this . computeEffectivePriority ( b , cache ) ;
13471433 const priDiff = bEffective . value - aEffective . value ;
13481434 if ( priDiff !== 0 ) return priDiff ;
1435+
1436+ // 2. Boost (descending) — only matters when effective priorities
1437+ // are equal. This includes blocks-high-priority boost and in-progress
1438+ // ancestor boost. Priority (1000) dominates boost (500) because
1439+ // boost is only consulted after effective priorities are confirmed equal.
1440+ const boostA = this . computeBoost ( a , now , ancestorsOfInProgress ) ;
1441+ const boostB = this . computeBoost ( b , now , ancestorsOfInProgress ) ;
1442+ const boostDiff = boostB - boostA ;
1443+ if ( boostDiff !== 0 ) return boostDiff ;
1444+
1445+ // 3. CreatedAt (ascending / oldest first)
13491446 const createdDiff = new Date ( a . createdAt ) . getTime ( ) - new Date ( b . createdAt ) . getTime ( ) ;
13501447 if ( createdDiff !== 0 ) return createdDiff ;
1448+
1449+ // 4. ID (deterministic tiebreaker)
13511450 return a . id . localeCompare ( b . id ) ;
13521451 } ) ;
13531452 return sorted [ 0 ] ?? null ;
0 commit comments