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

584 lines (499 loc) • 15.7 kB
/** * DAG Bridge for Obligation Tracking * * Provides directed acyclic graph operations for obligation dependency * tracking using ruvector-dag-wasm for high-performance graph algorithms. * * Features: * - Obligation dependency graph construction * - Critical path analysis * - Topological sorting * - Cycle detection * - Float/slack calculation * * Based on ADR-034: Legal Contract Analysis Plugin * * @module v3/plugins/legal-contracts/bridges/dag-bridge */ import type { IDAGBridge, Obligation, ObligationTrackingResult, ObligationNode, } from '../types.js'; /** * WASM module interface for DAG operations */ interface DAGWasmModule { /** Build adjacency list from edge list */ dag_build(edges: Uint32Array, nodeCount: number): number; /** Topological sort */ dag_topological_sort(graphPtr: number): Uint32Array; /** Detect cycles */ dag_detect_cycles(graphPtr: number): Uint32Array; /** Find critical path */ dag_critical_path( graphPtr: number, weights: Float32Array ): Uint32Array; /** Calculate longest path */ dag_longest_path( graphPtr: number, weights: Float32Array ): Float32Array; /** Free graph */ dag_free(graphPtr: number): void; /** Memory management */ alloc(size: number): number; dealloc(ptr: number, size: number): void; memory: WebAssembly.Memory; } /** * Edge type for dependency graph */ type EdgeType = 'depends_on' | 'blocks' | 'triggers'; /** * DAG Bridge Implementation */ export class DAGBridge implements IDAGBridge { // WASM module for future performance optimization (currently uses JS fallback) private wasmModule: DAGWasmModule | null = null; private initialized = false; /** * Initialize the WASM module */ async initialize(): Promise<void> { if (this.initialized) return; try { // Dynamic import of WASM module // In production, this would load from @claude-flow/ruvector-upstream this.wasmModule = await this.loadWasmModule(); this.initialized = true; } catch { // Fallback to pure JS implementation if WASM not available console.warn('WASM DAG module not available, using JS fallback'); this.wasmModule = null; this.initialized = true; } } /** * Check if initialized */ isInitialized(): boolean { return this.initialized; } /** * Build obligation dependency graph */ async buildDependencyGraph( obligations: Obligation[] ): Promise<ObligationTrackingResult['graph']> { if (!this.initialized) { await this.initialize(); } // Create node lookup const nodeMap = new Map<string, number>(); obligations.forEach((obl, index) => { nodeMap.set(obl.id, index); }); // Build edges from dependencies const edges: Array<{ from: string; to: string; type: EdgeType; }> = []; for (const obligation of obligations) { // depends_on edges (this obligation depends on others) for (const depId of obligation.dependsOn) { if (nodeMap.has(depId)) { edges.push({ from: depId, to: obligation.id, type: 'depends_on', }); } } // blocks edges (this obligation blocks others) for (const blockId of obligation.blocks) { if (nodeMap.has(blockId)) { edges.push({ from: obligation.id, to: blockId, type: 'blocks', }); } } } // Find critical path const criticalPathIds = await this.findCriticalPathInternal( obligations, nodeMap, edges ); // Calculate dates and float const { earliestStart, latestFinish, floatDays } = await this.calculateSchedule( obligations, nodeMap, edges ); // Build nodes const nodes: ObligationNode[] = obligations.map(obligation => { return { obligation, dependencies: obligation.dependsOn, dependents: obligation.blocks, onCriticalPath: criticalPathIds.has(obligation.id), earliestStart: earliestStart.get(obligation.id), latestFinish: latestFinish.get(obligation.id), floatDays: floatDays.get(obligation.id), }; }); return { nodes, edges }; } /** * Find critical path through obligations */ async findCriticalPath( graph: ObligationTrackingResult['graph'] ): Promise<string[]> { if (!this.initialized) { await this.initialize(); } // Extract obligations from nodes const obligations = graph.nodes.map(n => n.obligation); const nodeMap = new Map<string, number>(); obligations.forEach((obl, index) => { nodeMap.set(obl.id, index); }); const criticalIds = await this.findCriticalPathInternal( obligations, nodeMap, graph.edges ); // Return in topological order const sorted = await this.topologicalSort(obligations); return sorted .filter(o => criticalIds.has(o.id)) .map(o => o.id); } /** * Perform topological sort of obligations */ async topologicalSort(obligations: Obligation[]): Promise<Obligation[]> { if (!this.initialized) { await this.initialize(); } if (obligations.length === 0) { return []; } // Create node lookup const nodeMap = new Map<string, number>(); const indexMap = new Map<number, Obligation>(); obligations.forEach((obl, index) => { nodeMap.set(obl.id, index); indexMap.set(index, obl); }); // Build adjacency list const adj: number[][] = obligations.map(() => []); const inDegree = new Array(obligations.length).fill(0); for (const obligation of obligations) { const fromIndex = nodeMap.get(obligation.id); if (fromIndex === undefined) continue; for (const depId of obligation.blocks) { const toIndex = nodeMap.get(depId); if (toIndex !== undefined) { adj[fromIndex]?.push(toIndex); inDegree[toIndex] = (inDegree[toIndex] ?? 0) + 1; } } } // Kahn's algorithm const queue: number[] = []; for (let i = 0; i < inDegree.length; i++) { if (inDegree[i] === 0) { queue.push(i); } } const sorted: Obligation[] = []; while (queue.length > 0) { const current = queue.shift()!; const obligation = indexMap.get(current); if (obligation) { sorted.push(obligation); } for (const neighbor of adj[current] ?? []) { inDegree[neighbor] = (inDegree[neighbor] ?? 1) - 1; if (inDegree[neighbor] === 0) { queue.push(neighbor); } } } // If not all nodes are in sorted, there's a cycle if (sorted.length !== obligations.length) { console.warn('Cycle detected in obligation dependencies'); } return sorted; } /** * Detect cycles in dependency graph */ async detectCycles( graph: ObligationTrackingResult['graph'] ): Promise<string[][]> { if (!this.initialized) { await this.initialize(); } const obligations = graph.nodes.map(n => n.obligation); // Create node lookup const nodeMap = new Map<string, number>(); const indexMap = new Map<number, string>(); obligations.forEach((obl, index) => { nodeMap.set(obl.id, index); indexMap.set(index, obl.id); }); // Build adjacency list const adj: number[][] = obligations.map(() => []); for (const edge of graph.edges) { const fromIndex = nodeMap.get(edge.from); const toIndex = nodeMap.get(edge.to); if (fromIndex !== undefined && toIndex !== undefined) { adj[fromIndex]?.push(toIndex); } } // Find cycles using DFS const cycles: string[][] = []; const visited = new Array(obligations.length).fill(false); const recStack = new Array(obligations.length).fill(false); const parent = new Array(obligations.length).fill(-1); const findCycle = (node: number, path: number[]): void => { visited[node] = true; recStack[node] = true; path.push(node); for (const neighbor of adj[node] ?? []) { if (!visited[neighbor]) { parent[neighbor] = node; findCycle(neighbor, [...path]); } else if (recStack[neighbor]) { // Found cycle const cycleStart = path.indexOf(neighbor); if (cycleStart >= 0) { const cycle = path.slice(cycleStart).map(i => indexMap.get(i) ?? ''); cycles.push(cycle); } } } recStack[node] = false; }; for (let i = 0; i < obligations.length; i++) { if (!visited[i]) { findCycle(i, []); } } return cycles; } /** * Calculate slack/float for each obligation */ async calculateFloat( graph: ObligationTrackingResult['graph'], projectEnd: Date ): Promise<Map<string, number>> { if (!this.initialized) { await this.initialize(); } const obligations = graph.nodes.map(n => n.obligation); const nodeMap = new Map<string, number>(); obligations.forEach((obl, index) => { nodeMap.set(obl.id, index); }); const { floatDays } = await this.calculateSchedule( obligations, nodeMap, graph.edges, projectEnd ); return floatDays; } // ============================================================================ // Private Helper Methods // ============================================================================ /** * Load WASM module dynamically */ private async loadWasmModule(): Promise<DAGWasmModule> { // In production, this would load from @claude-flow/ruvector-upstream // For now, throw to trigger JS fallback throw new Error('WASM module loading not implemented'); } /** * Find critical path internally */ private async findCriticalPathInternal( obligations: Obligation[], nodeMap: Map<string, number>, edges: Array<{ from: string; to: string; type: EdgeType }> ): Promise<Set<string>> { const criticalIds = new Set<string>(); // Build adjacency list and weights const adj: Map<number, number[]> = new Map(); const weights: Map<number, number> = new Map(); for (let i = 0; i < obligations.length; i++) { adj.set(i, []); // Use estimated duration as weight (default 1 day) const obligation = obligations[i]; weights.set(i, this.estimateDuration(obligation)); } for (const edge of edges) { const fromIndex = nodeMap.get(edge.from); const toIndex = nodeMap.get(edge.to); if (fromIndex !== undefined && toIndex !== undefined) { adj.get(fromIndex)?.push(toIndex); } } // Find longest path using dynamic programming const sorted = await this.topologicalSort(obligations); const dist: Map<number, number> = new Map(); const predecessor: Map<number, number> = new Map(); // Initialize distances for (let i = 0; i < obligations.length; i++) { dist.set(i, 0); } // Process in topological order for (const obligation of sorted) { const u = nodeMap.get(obligation.id); if (u === undefined) continue; for (const v of adj.get(u) ?? []) { const newDist = (dist.get(u) ?? 0) + (weights.get(v) ?? 1); if (newDist > (dist.get(v) ?? 0)) { dist.set(v, newDist); predecessor.set(v, u); } } } // Find the end node with maximum distance let maxDist = 0; let endNode = 0; for (const [node, d] of dist) { if (d > maxDist) { maxDist = d; endNode = node; } } // Trace back critical path let current: number | undefined = endNode; while (current !== undefined) { const obligation = obligations[current]; if (obligation) { criticalIds.add(obligation.id); } current = predecessor.get(current); } return criticalIds; } /** * Calculate schedule (earliest start, latest finish, float) */ private async calculateSchedule( obligations: Obligation[], _nodeMap: Map<string, number>, edges: Array<{ from: string; to: string; type: EdgeType }>, projectEnd?: Date ): Promise<{ earliestStart: Map<string, Date>; latestFinish: Map<string, Date>; floatDays: Map<string, number>; }> { const now = new Date(); const endDate = projectEnd ?? new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000); const earliestStart = new Map<string, Date>(); const latestFinish = new Map<string, Date>(); const floatDays = new Map<string, number>(); // Build adjacency lists const successors: Map<string, string[]> = new Map(); const predecessors: Map<string, string[]> = new Map(); for (const obligation of obligations) { successors.set(obligation.id, []); predecessors.set(obligation.id, []); } for (const edge of edges) { successors.get(edge.from)?.push(edge.to); predecessors.get(edge.to)?.push(edge.from); } // Forward pass - calculate earliest start const sorted = await this.topologicalSort(obligations); for (const obligation of sorted) { const preds = predecessors.get(obligation.id) ?? []; let earliest = obligation.dueDate ?? now; for (const predId of preds) { const predObl = obligations.find(o => o.id === predId); const predEarliest = earliestStart.get(predId); if (predEarliest && predObl) { const predEnd = new Date( predEarliest.getTime() + this.estimateDuration(predObl) * 24 * 60 * 60 * 1000 ); if (predEnd > earliest) { earliest = predEnd; } } } earliestStart.set(obligation.id, earliest); } // Backward pass - calculate latest finish const reverseSorted = [...sorted].reverse(); for (const obligation of reverseSorted) { const succs = successors.get(obligation.id) ?? []; let latest = endDate; for (const succId of succs) { const succLatest = latestFinish.get(succId); if (succLatest && succLatest < latest) { latest = new Date( succLatest.getTime() - this.estimateDuration(obligation) * 24 * 60 * 60 * 1000 ); } } latestFinish.set(obligation.id, latest); } // Calculate float for (const obligation of obligations) { const es = earliestStart.get(obligation.id); const lf = latestFinish.get(obligation.id); if (es && lf) { const duration = this.estimateDuration(obligation); const latestStart = new Date(lf.getTime() - duration * 24 * 60 * 60 * 1000); const floatMs = latestStart.getTime() - es.getTime(); floatDays.set(obligation.id, Math.max(0, floatMs / (24 * 60 * 60 * 1000))); } else { floatDays.set(obligation.id, 0); } } return { earliestStart, latestFinish, floatDays }; } /** * Estimate duration in days for an obligation */ private estimateDuration(obligation: Obligation | undefined): number { if (!obligation) return 1; // Default durations by type const typeDurations: Record<string, number> = { payment: 1, delivery: 7, notification: 3, approval: 5, compliance: 30, reporting: 5, confidentiality: 0, performance: 14, insurance: 7, renewal: 30, termination: 30, }; return typeDurations[obligation.type] ?? 7; } } /** * Create and export default bridge instance */ export function createDAGBridge(): IDAGBridge { return new DAGBridge(); } export default DAGBridge;