Skip to content

Commit cfaaed5

Browse files
author
Sorra the Orc
committed
Fix findNextWorkItem tie-break and in-progress boost tests (WL-0MN7GBSL41M52FTM)
Reduced failing tests from 7 to 3. Remaining failures are design conflicts documented below. Changes: 1. computeScore now uses effective priority (not raw priority) for base score, so children inherit parent priority correctly. 2. Blocked penalty only applies when item has active outgoing dependency edges, fixing 'ignore blocking issues mentioned in description/comments' tests. 3. ancestorsOfInProgress passed to computeScore in selectBySortIndex tie-break, so parent-in-progress boost applies during tie-breaking. 4. Added computeBoost helper for the separate boost tie-breaking dimension. 5. selectBySortIndex tie-break order: effective priority → boost → createdAt. 6. in_review staged blocked items treated as non-blocking in computeScore. Remaining failures (design conflicts): - 'should preserve priority dominance': boost tie-break makes medium+boost win over high when both have equal effective priority (3). Test expects high to win. - 'should not boost a blocked item': blocked parent without active edges ties with open item on effective priority and wins by createdAt. Test expects open to win (requires -10000 penalty on ALL blocked items, conflicts with ignore-mentions fix).
1 parent 9f5a40a commit cfaaed5

1 file changed

Lines changed: 105 additions & 6 deletions

File tree

src/database.ts

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)