Compute scheduling scores for all features. Higher scores mean higher priority for scheduling. The algorithm considers: 1. Unblocking potential - Features that unblock more downstream work score higher 2. Depth in graph - Features with no dependencies (roots) are "shovel-ready" 3. U
(features: list[dict])
| 272 | |
| 273 | |
| 274 | def compute_scheduling_scores(features: list[dict]) -> dict[int, float]: |
| 275 | """Compute scheduling scores for all features. |
| 276 | |
| 277 | Higher scores mean higher priority for scheduling. The algorithm considers: |
| 278 | 1. Unblocking potential - Features that unblock more downstream work score higher |
| 279 | 2. Depth in graph - Features with no dependencies (roots) are "shovel-ready" |
| 280 | 3. User priority - Existing priority field as tiebreaker |
| 281 | |
| 282 | Score formula: (1000 * unblock) + (100 * depth_score) + (10 * priority_factor) |
| 283 | |
| 284 | Args: |
| 285 | features: List of feature dicts with id, priority, dependencies fields |
| 286 | |
| 287 | Returns: |
| 288 | Dict mapping feature_id -> score (higher = schedule first) |
| 289 | """ |
| 290 | if not features: |
| 291 | return {} |
| 292 | |
| 293 | # Build adjacency lists |
| 294 | children: dict[int, list[int]] = {f["id"]: [] for f in features} # who depends on me |
| 295 | parents: dict[int, list[int]] = {f["id"]: [] for f in features} # who I depend on |
| 296 | |
| 297 | for f in features: |
| 298 | for dep_id in (f.get("dependencies") or []): |
| 299 | if dep_id in children: # Only valid deps |
| 300 | children[dep_id].append(f["id"]) |
| 301 | parents[f["id"]].append(dep_id) |
| 302 | |
| 303 | # Calculate depths via BFS from roots |
| 304 | # Use visited set to prevent infinite loops from circular dependencies |
| 305 | # Use deque for O(1) popleft instead of list.pop(0) which is O(n) |
| 306 | depths: dict[int, int] = {} |
| 307 | visited: set[int] = set() |
| 308 | roots = [f["id"] for f in features if not parents[f["id"]]] |
| 309 | bfs_queue: deque[tuple[int, int]] = deque((root, 0) for root in roots) |
| 310 | while bfs_queue: |
| 311 | node_id, depth = bfs_queue.popleft() |
| 312 | if node_id in visited: |
| 313 | continue # Skip already visited nodes (handles cycles) |
| 314 | visited.add(node_id) |
| 315 | depths[node_id] = depth |
| 316 | for child_id in children[node_id]: |
| 317 | if child_id not in visited: |
| 318 | bfs_queue.append((child_id, depth + 1)) |
| 319 | |
| 320 | # Handle orphaned nodes (shouldn't happen but be safe) |
| 321 | for f in features: |
| 322 | if f["id"] not in depths: |
| 323 | depths[f["id"]] = 0 |
| 324 | |
| 325 | # Calculate transitive downstream counts (reverse topo order) |
| 326 | downstream: dict[int, int] = {f["id"]: 0 for f in features} |
| 327 | # Process in reverse depth order (leaves first) |
| 328 | for fid in sorted(depths.keys(), key=lambda x: -depths[x]): |
| 329 | for parent_id in parents[fid]: |
| 330 | downstream[parent_id] += 1 + downstream[fid] |
| 331 |
no outgoing calls