claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
653 lines (559 loc) • 17.6 kB
text/typescript
/**
* MinCut Bridge for Module Splitting
*
* Provides graph min-cut operations for optimal module boundary detection
* using ruvector-mincut-wasm for high-performance graph partitioning.
*
* Features:
* - Optimal module boundary detection
* - Multi-way graph partitioning
* - Constraint-aware splitting
* - Cut weight optimization
*
* Based on ADR-035: Advanced Code Intelligence Plugin
*
* @module v3/plugins/code-intelligence/bridges/mincut-bridge
*/
import type {
IMinCutBridge,
DependencyGraph,
SplitConstraints,
} from '../types.js';
/**
* WASM module interface for MinCut operations
*/
interface MinCutWasmModule {
/** Build flow network from graph */
mincut_build_network(
nodeCount: number,
edges: Uint32Array,
capacities: Float32Array,
edgeCount: number
): number;
/** Find min s-t cut using Ford-Fulkerson */
mincut_ford_fulkerson(
networkPtr: number,
source: number,
sink: number
): Float32Array;
/** Multi-way cut using recursive bisection */
mincut_multiway(
networkPtr: number,
terminals: Uint32Array,
numTerminals: number
): Uint32Array;
/** Spectral partitioning */
mincut_spectral_partition(
networkPtr: number,
numPartitions: number,
weights: Float32Array
): Uint32Array;
/** Free network */
mincut_free(networkPtr: number): void;
/** Memory management */
alloc(size: number): number;
dealloc(ptr: number, size: number): void;
memory: WebAssembly.Memory;
}
/**
* MinCut Bridge Implementation
*/
export class MinCutBridge implements IMinCutBridge {
// WASM module for future performance optimization (currently uses JS fallback)
private wasmModule: MinCutWasmModule | null = null;
private initialized = false;
/**
* Initialize the WASM module
*/
async initialize(): Promise<void> {
if (this.initialized) return;
try {
// Dynamic import of WASM module
this.wasmModule = await this.loadWasmModule();
this.initialized = true;
} catch {
// Fallback to pure JS implementation
console.warn('WASM MinCut module not available, using JS fallback');
this.wasmModule = null;
this.initialized = true;
}
}
/**
* Check if initialized
*/
isInitialized(): boolean {
return this.initialized;
}
/**
* Find optimal module boundaries using MinCut
*/
async findOptimalCuts(
graph: DependencyGraph,
numModules: number,
constraints: SplitConstraints
): Promise<Map<string, number>> {
if (!this.initialized) {
await this.initialize();
}
const partition = new Map<string, number>();
const nodeCount = graph.nodes.length;
if (nodeCount === 0 || numModules < 2) {
// All nodes in single partition
for (const node of graph.nodes) {
partition.set(node.id, 0);
}
return partition;
}
// Create node lookup
const nodeMap = new Map<string, number>();
const indexMap = new Map<number, string>();
graph.nodes.forEach((node, index) => {
nodeMap.set(node.id, index);
indexMap.set(index, node.id);
});
// Apply constraints for preserved boundaries
const fixed = new Map<number, number>();
if (constraints.preserveBoundaries) {
for (let i = 0; i < constraints.preserveBoundaries.length && i < numModules; i++) {
const boundary = constraints.preserveBoundaries[i];
if (boundary) {
const nodeIdx = nodeMap.get(boundary);
if (nodeIdx !== undefined) {
fixed.set(nodeIdx, i);
}
}
}
}
// Build adjacency matrix with weights
const weights: number[][] = Array.from({ length: nodeCount }, () =>
Array(nodeCount).fill(0)
);
for (const edge of graph.edges) {
const fromIdx = nodeMap.get(edge.from);
const toIdx = nodeMap.get(edge.to);
if (fromIdx !== undefined && toIdx !== undefined) {
weights[fromIdx]![toIdx] = edge.weight;
weights[toIdx]![fromIdx] = edge.weight; // Symmetric for partitioning
}
}
// Use spectral partitioning (JS fallback)
const partitionArray = this.spectralPartition(
weights,
numModules,
fixed,
constraints
);
// Convert to map
for (let i = 0; i < nodeCount; i++) {
const nodeId = indexMap.get(i);
const part = partitionArray[i];
if (nodeId && part !== undefined) {
partition.set(nodeId, part);
}
}
// Apply keepTogether constraints
if (constraints.keepTogether) {
for (const group of constraints.keepTogether) {
if (group.length > 0) {
const firstIdx = nodeMap.get(group[0] ?? '');
const firstPart = firstIdx !== undefined ? partitionArray[firstIdx] : undefined;
if (firstPart !== undefined) {
for (const nodeId of group) {
partition.set(nodeId, firstPart);
}
}
}
}
}
return partition;
}
/**
* Calculate cut weight for a given partition
*/
async calculateCutWeight(
graph: DependencyGraph,
partition: Map<string, number>
): Promise<number> {
if (!this.initialized) {
await this.initialize();
}
let cutWeight = 0;
for (const edge of graph.edges) {
const fromPart = partition.get(edge.from);
const toPart = partition.get(edge.to);
if (fromPart !== undefined && toPart !== undefined && fromPart !== toPart) {
cutWeight += edge.weight;
}
}
return cutWeight;
}
/**
* Find minimum s-t cut
*/
async minSTCut(
graph: DependencyGraph,
source: string,
sink: string
): Promise<{
cutValue: number;
cutEdges: Array<{ from: string; to: string }>;
sourceSet: string[];
sinkSet: string[];
}> {
if (!this.initialized) {
await this.initialize();
}
const nodeCount = graph.nodes.length;
// Create node lookup
const nodeMap = new Map<string, number>();
const indexMap = new Map<number, string>();
graph.nodes.forEach((node, index) => {
nodeMap.set(node.id, index);
indexMap.set(index, node.id);
});
const sourceIdx = nodeMap.get(source);
const sinkIdx = nodeMap.get(sink);
if (sourceIdx === undefined || sinkIdx === undefined) {
return {
cutValue: 0,
cutEdges: [],
sourceSet: [],
sinkSet: graph.nodes.map(n => n.id),
};
}
// Build capacity matrix
const capacity: number[][] = Array.from({ length: nodeCount }, () =>
Array(nodeCount).fill(0)
);
for (const edge of graph.edges) {
const fromIdx = nodeMap.get(edge.from);
const toIdx = nodeMap.get(edge.to);
if (fromIdx !== undefined && toIdx !== undefined) {
capacity[fromIdx]![toIdx] = edge.weight;
}
}
// Ford-Fulkerson with BFS (Edmonds-Karp)
const { maxFlow, residual } = this.edmondsKarp(capacity, sourceIdx, sinkIdx);
// Find source set using BFS on residual graph
const sourceSet = new Set<number>();
const queue = [sourceIdx];
sourceSet.add(sourceIdx);
while (queue.length > 0) {
const current = queue.shift()!;
for (let next = 0; next < nodeCount; next++) {
if (!sourceSet.has(next) && (residual[current]?.[next] ?? 0) > 0) {
sourceSet.add(next);
queue.push(next);
}
}
}
// Find cut edges
const cutEdges: Array<{ from: string; to: string }> = [];
for (const edge of graph.edges) {
const fromIdx = nodeMap.get(edge.from);
const toIdx = nodeMap.get(edge.to);
if (fromIdx !== undefined && toIdx !== undefined) {
if (sourceSet.has(fromIdx) && !sourceSet.has(toIdx)) {
cutEdges.push({ from: edge.from, to: edge.to });
}
}
}
// Convert sets to arrays
const sourceSetNodes: string[] = [];
const sinkSetNodes: string[] = [];
for (let i = 0; i < nodeCount; i++) {
const nodeId = indexMap.get(i);
if (nodeId) {
if (sourceSet.has(i)) {
sourceSetNodes.push(nodeId);
} else {
sinkSetNodes.push(nodeId);
}
}
}
return {
cutValue: maxFlow,
cutEdges,
sourceSet: sourceSetNodes,
sinkSet: sinkSetNodes,
};
}
/**
* Multi-way cut for module splitting
*/
async multiWayCut(
graph: DependencyGraph,
terminals: string[],
_weights: Map<string, number>
): Promise<{
cutValue: number;
partitions: Map<string, number>;
}> {
if (!this.initialized) {
await this.initialize();
}
const numTerminals = terminals.length;
if (numTerminals < 2) {
const partitions = new Map<string, number>();
for (const node of graph.nodes) {
partitions.set(node.id, 0);
}
return { cutValue: 0, partitions };
}
// Create node lookup
const nodeMap = new Map<string, number>();
graph.nodes.forEach((node, index) => {
nodeMap.set(node.id, index);
});
// Get terminal indices
const terminalIndices = terminals
.map(t => nodeMap.get(t))
.filter((idx): idx is number => idx !== undefined);
if (terminalIndices.length < 2) {
const partitions = new Map<string, number>();
for (const node of graph.nodes) {
partitions.set(node.id, 0);
}
return { cutValue: 0, partitions };
}
// Use isolating cuts algorithm
// Assign each node to the nearest terminal
const partitions = new Map<string, number>();
const distances = this.computeDistances(graph, terminalIndices, nodeMap);
for (const node of graph.nodes) {
const nodeIdx = nodeMap.get(node.id);
if (nodeIdx === undefined) continue;
let minDist = Infinity;
let minTerminal = 0;
for (let t = 0; t < terminalIndices.length; t++) {
const dist = distances.get(`${nodeIdx}-${t}`) ?? Infinity;
if (dist < minDist) {
minDist = dist;
minTerminal = t;
}
}
partitions.set(node.id, minTerminal);
}
// Calculate cut value
const cutValue = await this.calculateCutWeight(graph, partitions);
return { cutValue, partitions };
}
// ============================================================================
// Private Helper Methods
// ============================================================================
/**
* Load WASM module dynamically
*/
private async loadWasmModule(): Promise<MinCutWasmModule> {
throw new Error('WASM module loading not implemented');
}
/**
* Spectral partitioning using Fiedler vector
*/
private spectralPartition(
weights: number[][],
numPartitions: number,
fixed: Map<number, number>,
_constraints: SplitConstraints
): number[] {
const n = weights.length;
const partition = new Array(n).fill(0);
if (n === 0) return partition;
// Apply fixed partitions
for (const [node, part] of fixed) {
partition[node] = part;
}
// If all fixed, return
if (fixed.size >= n) return partition;
// Compute Laplacian
const laplacian: number[][] = Array.from({ length: n }, () =>
Array(n).fill(0)
);
for (let i = 0; i < n; i++) {
let degree = 0;
for (let j = 0; j < n; j++) {
if (i !== j) {
const w = (weights[i]?.[j] ?? 0) + (weights[j]?.[i] ?? 0);
laplacian[i]![j] = -w;
degree += w;
}
}
laplacian[i]![i] = degree;
}
// Power iteration to find Fiedler vector (second smallest eigenvector)
// Simplified: use random initialization and iterate
const fiedler = new Array(n).fill(0).map(() => Math.random() - 0.5);
// Normalize
let norm = Math.sqrt(fiedler.reduce((sum, v) => sum + v * v, 0));
for (let i = 0; i < n; i++) {
fiedler[i] = (fiedler[i] ?? 0) / norm;
}
// Iterate
for (let iter = 0; iter < 50; iter++) {
// Multiply by Laplacian
const newFiedler = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
newFiedler[i] += (laplacian[i]?.[j] ?? 0) * (fiedler[j] ?? 0);
}
}
// Orthogonalize against constant vector
const mean = newFiedler.reduce((a, b) => a + b, 0) / n;
for (let i = 0; i < n; i++) {
newFiedler[i] = (newFiedler[i] ?? 0) - mean;
}
// Normalize
norm = Math.sqrt(newFiedler.reduce((sum, v) => sum + v * v, 0));
if (norm > 0) {
for (let i = 0; i < n; i++) {
fiedler[i] = (newFiedler[i] ?? 0) / norm;
}
}
}
// Partition based on Fiedler vector
if (numPartitions === 2) {
// Simple bisection
for (let i = 0; i < n; i++) {
if (!fixed.has(i)) {
partition[i] = (fiedler[i] ?? 0) >= 0 ? 0 : 1;
}
}
} else {
// K-means clustering on Fiedler values
const sorted = fiedler
.map((v, i) => ({ value: v, index: i }))
.filter(item => !fixed.has(item.index))
.sort((a, b) => (a.value ?? 0) - (b.value ?? 0));
const binSize = Math.ceil(sorted.length / numPartitions);
for (let i = 0; i < sorted.length; i++) {
const item = sorted[i];
if (item) {
partition[item.index] = Math.min(Math.floor(i / binSize), numPartitions - 1);
}
}
}
return partition;
}
/**
* Edmonds-Karp algorithm (Ford-Fulkerson with BFS)
*/
private edmondsKarp(
capacity: number[][],
source: number,
sink: number
): { maxFlow: number; residual: number[][] } {
const n = capacity.length;
const residual: number[][] = capacity.map(row => [...row]);
let maxFlow = 0;
// BFS to find augmenting path
const bfs = (): number[] | null => {
const parent = new Array(n).fill(-1);
const visited = new Array(n).fill(false);
const queue = [source];
visited[source] = true;
while (queue.length > 0) {
const current = queue.shift()!;
if (current === sink) {
// Reconstruct path
const path: number[] = [];
let node = sink;
while (node !== source) {
path.unshift(node);
node = parent[node] ?? source;
}
path.unshift(source);
return path;
}
for (let next = 0; next < n; next++) {
if (!visited[next] && (residual[current]?.[next] ?? 0) > 0) {
visited[next] = true;
parent[next] = current;
queue.push(next);
}
}
}
return null;
};
// Find augmenting paths
let path = bfs();
while (path !== null) {
// Find minimum capacity along path
let minCap = Infinity;
for (let i = 0; i < path.length - 1; i++) {
const from = path[i];
const to = path[i + 1];
if (from !== undefined && to !== undefined) {
minCap = Math.min(minCap, residual[from]?.[to] ?? 0);
}
}
// Update residual capacities
for (let i = 0; i < path.length - 1; i++) {
const from = path[i];
const to = path[i + 1];
if (from !== undefined && to !== undefined) {
residual[from]![to] = (residual[from]?.[to] ?? 0) - minCap;
residual[to]![from] = (residual[to]?.[from] ?? 0) + minCap;
}
}
maxFlow += minCap;
path = bfs();
}
return { maxFlow, residual };
}
/**
* Compute distances from terminals to all nodes
*/
private computeDistances(
graph: DependencyGraph,
terminalIndices: number[],
nodeMap: Map<string, number>
): Map<string, number> {
const distances = new Map<string, number>();
const nodeCount = graph.nodes.length;
// Build adjacency list with weights
const adj: Map<number, Array<{ to: number; weight: number }>> = new Map();
for (let i = 0; i < nodeCount; i++) {
adj.set(i, []);
}
for (const edge of graph.edges) {
const fromIdx = nodeMap.get(edge.from);
const toIdx = nodeMap.get(edge.to);
if (fromIdx !== undefined && toIdx !== undefined) {
adj.get(fromIdx)?.push({ to: toIdx, weight: edge.weight });
adj.get(toIdx)?.push({ to: fromIdx, weight: edge.weight });
}
}
// Dijkstra from each terminal
for (let t = 0; t < terminalIndices.length; t++) {
const terminal = terminalIndices[t];
if (terminal === undefined) continue;
const dist = new Array(nodeCount).fill(Infinity);
dist[terminal] = 0;
const pq: Array<{ node: number; dist: number }> = [{ node: terminal, dist: 0 }];
while (pq.length > 0) {
pq.sort((a, b) => a.dist - b.dist);
const current = pq.shift()!;
if (current.dist > (dist[current.node] ?? Infinity)) continue;
for (const neighbor of adj.get(current.node) ?? []) {
const newDist = current.dist + (1 / Math.max(neighbor.weight, 0.1)); // Inverse weight as distance
if (newDist < (dist[neighbor.to] ?? Infinity)) {
dist[neighbor.to] = newDist;
pq.push({ node: neighbor.to, dist: newDist });
}
}
}
// Store distances
for (let i = 0; i < nodeCount; i++) {
distances.set(`${i}-${t}`, dist[i] ?? Infinity);
}
}
return distances;
}
}
/**
* Create and export default bridge instance
*/
export function createMinCutBridge(): IMinCutBridge {
return new MinCutBridge();
}
export default MinCutBridge;