feat(analysis/bottlenecks): implement bottlenecks function using graphology-metrics betweenness centrality

- Implements bottlenecks(graph: TaskGraph): BottleneckResult[] with normalized scores (0.0-1.0)
- Uses graphology-metrics centrality.betweenness with normalized: true
- Returns array sorted by score descending, includes all nodes (even score 0)
- Handles empty graph edge case (returns empty array)
- 20 unit tests covering: linear chain, star graph, independent nodes,
  disconnected graph, diamond, empty graph, single node
This commit is contained in:
2026-04-27 12:35:48 +00:00
parent b0d943f4e6
commit 62e23b5989
2 changed files with 337 additions and 4 deletions

View File

@@ -1 +1,64 @@
// bottlenecks (graphology betweenness)
// bottlenecks (graphology betweenness)
import { centrality } from 'graphology-metrics';
import type { TaskGraph } from '../graph/index.js';
/**
* Result of bottleneck analysis: a task ID paired with its betweenness centrality score.
*
* Higher scores indicate the task lies on more shortest paths between other
* nodes, making it a structural bottleneck — delaying or failing this task
* has outsized impact on the overall workflow.
*/
export interface BottleneckResult {
taskId: string;
score: number;
}
/**
* Compute bottleneck scores for all tasks in the graph using betweenness centrality.
*
* Betweenness centrality measures the fraction of shortest paths between all
* node pairs that pass through a given node. Nodes with high betweenness are
* structural bottlenecks: they sit on the most shortest paths and their
* delay or failure disrupts the most communication/routes in the graph.
*
* Uses `graphology-metrics` betweenness centrality with `normalized: true`,
* which produces scores in the **0.01.0** range. For disconnected graphs,
* betweenness is 0 for nodes in components with fewer than 3 nodes (no
* shortest paths can traverse through them between distinct endpoints).
*
* All tasks are included in the result, even those with score 0 (they are
* not bottlenecks). Results are sorted by score descending (most critical
* bottlenecks first).
*
* @param graph - The task graph to analyze
* @returns Array of `{ taskId, score }` sorted by score descending
*/
export function bottlenecks(graph: TaskGraph): BottleneckResult[] {
const raw = graph.raw;
// Edge case: empty graph — graphology-metrics betweenness centrality
// throws on an empty graph (mnemonist FixedStack requires positive capacity).
// Return an empty array since there are no nodes to score.
if (raw.order === 0) {
return [];
}
// Compute normalized betweenness centrality (0.01.0 range)
const centralityMap = centrality.betweenness(raw, { normalized: true });
// Map to result objects for all nodes in the graph
const results: BottleneckResult[] = [];
raw.forEachNode((node) => {
results.push({
taskId: node,
score: centralityMap[node] ?? 0,
});
});
// Sort by score descending (highest bottleneck first)
results.sort((a, b) => b.score - a.score);
return results;
}