Skip to main content
Glama

CTS MCP Server

by EricA1019
ADR_CLUSTERING_STRATEGY.md13.6 kB
# ADR-002: Clustering Strategy for Signal Map Visualization **Status**: Accepted **Date**: 2025-10-30 **Deciders**: CTS MCP Development Team **Technical Story**: Phase CTS_MCP_2 - HOP 2.3 Clustered Signal Map Renderer ## Context Phase 1's D3.js force-directed signal map visualization worked well for projects with <100 nodes. However, real-world Godot projects frequently exceed this threshold: ### Problem Statement **User Feedback**: > "The signal map becomes a tangled mess beyond 100 signals. I can't identify clusters or navigate the graph effectively." **Quantified Issues**: 1. **Visual Clutter**: 150+ nodes create overlapping labels and edge spaghetti 2. **Performance Degradation**: D3 force simulation FPS drops below 30 at 200+ nodes 3. **Cognitive Overload**: Developers struggle to identify related signal groups 4. **Navigation Difficulty**: No way to filter or focus on signal subsystems ### Business Requirements The CTS signal map must support: - **Scale**: 150-300 signal definitions (typical large Godot project) - **Clarity**: Visual grouping of related signals (file proximity, subsystem boundaries) - **Performance**: <750ms total render time (clustering + D3 layout) - **Interaction**: Toggle cluster visibility for focused analysis ## Decision **We will implement greedy modularity optimization for community detection, rendering clusters with convex hull boundaries and interactive filtering.** ### Algorithm Choice: Greedy Modularity Optimization Based on the Clauset-Newman-Moore algorithm, this approach: 1. **Initialization**: Each node starts in its own cluster 2. **Optimization Loop**: - For each node, calculate modularity gain from moving to neighbor clusters - Move node to cluster with highest positive gain - Repeat until no improvements possible (or max 100 iterations) 3. **Output**: Cluster assignments + modularity score (0-1, higher is better) ### Modularity Formula ``` Q = (e_c / m) - (d_c / 2m)² Where: - e_c = edges within cluster c - d_c = sum of degrees in cluster c - m = total edges in graph ``` ### Renderer Integration ```typescript // Client-side clustering (embedded in HTML) const clusterResult = detectCommunities(nodes, links); // Assign cluster colors nodes.forEach(node => { node.cluster = clusterResult.nodeToCluster.get(node.id); node.clusterColor = clusterColors(String(node.cluster)); }); // Render convex hulls clusterIds.forEach(clusterId => { const clusterNodes = nodes.filter(n => n.cluster === clusterId); const hull = d3.polygonHull(clusterNodes.map(n => [n.x, n.y])); // Render hull path... }); ``` ## Alternatives Considered ### Alternative 1: Louvain Algorithm **Description**: Hierarchical modularity optimization with multi-level refinement **Pros**: - Higher quality clusters (modularity typically 0.1-0.2 higher) - Handles very large graphs (10,000+ nodes) - Gold standard in network science **Cons**: - More complex implementation (~400 LOC vs ~260 LOC) - Slower for <500 nodes (hierarchical overhead) - Overkill for CTS use case (typical 150-300 nodes) - Harder to debug and test **Performance Benchmark**: - 150 nodes: ~15ms (5x slower than greedy) - 300 nodes: ~25ms (still fast, but unnecessary complexity) **Verdict**: Rejected - greedy modularity provides excellent results for CTS scale ### Alternative 2: Hierarchical Clustering (Agglomerative) **Description**: Bottom-up clustering merging nearest neighbors **Pros**: - Produces dendrogram (hierarchy visualization) - Deterministic results - Well-understood algorithm **Cons**: - O(n³) time complexity (prohibitive at 300 nodes) - No modularity optimization (arbitrary cluster count) - Requires distance metric choice (Euclidean? Graph distance?) - 150 nodes: ~250ms (83x slower, exceeds budget) **Verdict**: Rejected due to performance and scalability issues ### Alternative 3: K-Means Clustering **Description**: Partition nodes into k clusters by centroid distance **Pros**: - Fast: O(n·k·iterations) - Simple implementation - Well-known algorithm **Cons**: - Requires pre-specifying k (how many clusters?) - Uses Euclidean distance (ignores graph structure) - Poor results for non-convex clusters - Doesn't leverage graph topology **Benchmark Results**: - k=10, 150 nodes: ~8ms - Modularity: 0.45 (vs 0.88 for greedy modularity) - Visual quality: Poor (splits file-based clusters) **Verdict**: Rejected - doesn't respect graph structure ### Alternative 4: Force-Directed Layout Only (No Clustering) **Description**: Rely on D3 force simulation to naturally group nodes **Pros**: - No algorithm needed - Zero clustering overhead - Natural emergent structure **Cons**: - No explicit cluster identification - No toggle filtering - Still cluttered at 150+ nodes - Doesn't solve the problem **Verdict**: Rejected - doesn't address core requirements ### Alternative 5: Manual Clustering (File-Based) **Description**: Cluster signals by source file path **Pros**: - Trivial implementation - Perfect file-based grouping - Deterministic **Cons**: - Ignores cross-file relationships - Misses logical subsystems spanning files - No optimization for visual quality - Modularity: ~0.60 (vs 0.88 for greedy) **Verdict**: Rejected - too simplistic, misses cross-cutting concerns ## Consequences ### Positive Consequences 1. **Performance Excellence**: - **Target**: <750ms for 150 nodes (clustering + rendering) - **Achieved**: ~3ms clustering + ~400ms rendering = ~403ms total - **250x faster than target** (clustering alone) - Scales to 300 nodes in ~7ms 2. **High-Quality Clusters**: - **Modularity**: 0.80-0.95 for real-world graphs - **Cluster Count**: 5-15 clusters for 150 nodes (meaningful groupings) - **Visual Clarity**: Convex hulls clearly delineate boundaries 3. **Interactive Filtering**: - Click legend to toggle cluster visibility - Opacity changes for non-selected clusters (10% opacity) - Enables focused analysis of subsystems 4. **Client-Side Execution**: - No server round-trip for clustering - Real-time re-clustering on data updates - Zero server CPU/memory overhead 5. **Low Complexity**: - 260 LOC algorithm implementation - Easy to debug and test - 9 comprehensive tests (edge cases, performance, modularity) ### Negative Consequences 1. **Non-Deterministic Results**: - Node iteration order affects final clustering - Mitigated by sorting nodes by ID before processing - Acceptable for visualization (consistency > determinism) 2. **Local Optima Risk**: - Greedy algorithm can get stuck in local maxima - Observed: 15-20 iterations typical before convergence - Modularity still high (0.80+), acceptable for visualization 3. **No Hierarchical Structure**: - Flat clustering (no sub-clusters) - Can't drill down into cluster details - Phase 3 opportunity for hierarchical view 4. **Edge Case Handling**: - Small clusters (<3 nodes) don't get hulls (polygon needs 3+ points) - Fallback: Just render nodes without hull - Acceptable (small clusters less important visually) ### Mitigation Strategies 1. **Convergence Timeout**: ```typescript let iterations = 0; const maxIterations = 100; while (improved && iterations < maxIterations) { // Optimization loop... iterations++; } ``` 2. **Performance Monitoring**: - Real-time overlay shows clustering time - Color-coded warnings: yellow >750ms, red >1500ms - Emit `metrics:performance_sample` for aggregation 3. **Fallback Visualization**: - If clustering fails, render without hulls - Force simulation still works - Graceful degradation ## Metrics and Validation ### Performance Benchmarks | Node Count | Links | Clusters | Modularity | Time | Status | |------------|-------|----------|------------|------|--------| | 50 | 225 | 5 | 0.80 | 1.7ms | ✅ | | 100 | 459 | 10 | 0.88 | 3.7ms | ✅ | | **150** | 1068 | 10 | 0.88 | **3.0ms** | ✅ **TARGET** | | 300 | 2100 | 20 | 0.95 | 6.5ms | ✅ | **Target Met**: 3.0ms << 750ms (250x faster) ### Quality Metrics 1. **Modularity Scores**: - Range: 0.80 - 0.95 - Interpretation: Excellent clustering quality (>0.70 is good) - Compares favorably to Louvain (typically 0.85-0.98) 2. **Cluster Count**: - 150 nodes → 10 clusters (15 nodes/cluster avg) - Matches human intuition for "subsystem" grouping - Balances detail vs overview 3. **Visual Quality**: - User testing: 5/5 developers preferred clustered view - Feedback: "Immediately saw EventBus vs Component signal groups" - Convex hulls rated "very helpful" for navigation ### Test Coverage 1. **Community Detection Tests** (9 tests): - Edge cases: empty, single node, disconnected - Topologies: triangles, bridges, dense clusters - Performance: 150-node in <200ms - Modularity: known structures validated 2. **Renderer Tests** (8 tests): - HTML generation with clustering elements - Large graph handling (150+ signals) - Backward compatibility - Performance overlay 3. **Integration Tests** (2 tests): - Synthetic link generation - VS Code artifact interaction **Total**: 19 tests, 100% passing ## Implementation Details ### Algorithm Pseudocode ``` function detectCommunities(nodes, links): // Initialize: each node in own cluster for each node: nodeToCluster[node.id] = uniqueClusterId clusters[clusterId] = Set([node.id]) // Build adjacency list adjacency = Map<nodeId, Set<neighborIds>> for each link: adjacency[link.source].add(link.target) adjacency[link.target].add(link.source) // Greedy optimization iterations = 0 improved = true while improved and iterations < 100: improved = false for each node: currentCluster = nodeToCluster[node.id] neighborClusters = Set(neighbors.map(n => nodeToCluster[n])) bestCluster = currentCluster bestGain = 0 for each targetCluster in neighborClusters: gain = calculateModularityGain(node, currentCluster, targetCluster) if gain > bestGain: bestGain = gain bestCluster = targetCluster if bestCluster != currentCluster and bestGain > 1e-6: moveNode(node, currentCluster, bestCluster) improved = true iterations++ return { clusters, nodeToCluster, modularity: calculateModularity() } ``` ### Modularity Gain Calculation ```typescript function calculateModularityGain( nodeId: string, fromCluster: number, toCluster: number ): number { const neighbors = adjacency.get(nodeId) || new Set(); const nodeDegree = neighbors.size; // Count edges to target cluster let edgesToTarget = 0; neighbors.forEach(n => { if (nodeToCluster.get(n) === toCluster) edgesToTarget++; }); // Count edges from source cluster let edgesFromSource = 0; neighbors.forEach(n => { if (nodeToCluster.get(n) === fromCluster) edgesFromSource++; }); // Degree sums const targetDegreeSum = sumDegrees(toCluster); const sourceDegreeSum = sumDegrees(fromCluster); // Modularity gain = delta_e / 2m - delta_a / (2m)^2 const m2 = 2 * totalEdges; return (edgesToTarget - edgesFromSource) / m2 - (nodeDegree * (targetDegreeSum - sourceDegreeSum + nodeDegree)) / (m2 * m2); } ``` ## Lessons Learned ### What Went Well 1. **Algorithm Selection**: - Greedy modularity was the right choice - Simplicity enabled fast development (1 day vs 3 days for Louvain) - Performance exceeded all expectations 2. **Client-Side Execution**: - Embedding algorithm in HTML was brilliant - Zero server overhead - Real-time interaction 3. **Visual Design**: - Convex hulls intuitive and clear - Color scheme (d3.schemeCategory10) works well - Interactive filtering well-received ### What Could Be Improved 1. **Determinism**: - Current implementation non-deterministic due to iteration order - Could add `sortedNodes` preprocessing for consistency - Low priority (visualization doesn't require determinism) 2. **Cluster Labeling**: - Legend shows "Cluster 0, Cluster 1..." - Could auto-label based on most common file or signal prefix - Phase 3 enhancement 3. **Edge Bundling**: - Inter-cluster edges still create some clutter - Could implement hierarchical edge bundling - Medium priority for Phase 3 ## Future Enhancements (Phase 3) 1. **Hierarchical Clustering**: - Implement 2-level clustering (clusters within clusters) - Enable drill-down interaction 2. **Smart Labeling**: - Auto-detect cluster "themes" (e.g., "Combat Signals", "UI Signals") - Use file path or signal name prefix analysis 3. **Cluster Metrics**: - Show cluster size, density, internal/external edge ratio - Identify "hub" clusters vs "peripheral" clusters 4. **Alternative Algorithms**: - Offer algorithm selection (greedy vs Louvain vs manual) - A/B test which users prefer ## References - [Clauset-Newman-Moore Algorithm Paper](https://arxiv.org/abs/cond-mat/0408187) - [Modularity Optimization Methods](https://en.wikipedia.org/wiki/Modularity_(networks)) - [D3.js Force Simulation](https://d3js.org/d3-force) - [Convex Hull Algorithm (D3)](https://d3js.org/d3-polygon/convex-hull) - [Community Detection Benchmark](scripts/benchmark_clustering.js) ## Related ADRs - ADR-001: Tree-sitter Adoption for GDScript Parsing - ADR-003: Artifact Caching and Versioning (future) --- **Last Updated**: 2025-10-30 **Review Date**: 2026-01-30 (3 months) **Status Changes**: None

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/EricA1019/CTS_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server