UNPKG

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
/** * 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;