How the Louvain Algorithm Detects Functional Modules in Codebase-Memory-MCP
The Louvain algorithm detects functional modules by clustering functions into communities based on call-graph density, using a Leiden-refined modularity optimization implemented in C that processes weighted edges in src/store/store.c.
The codebase-memory-mcp repository employs graph-based community detection to automatically identify functional modules—groups of tightly coupled functions—within indexed codebases. By treating function calls as edges in an undirected graph, the system leverages the Louvain algorithm with Leiden refinements to maximize modularity and expose architectural boundaries. This analysis traces the exact implementation path from raw call extraction to the final get_architecture response.
Graph Construction: From Function Calls to Weighted Nodes
During the indexing phase, the system transforms the codebase into a mathematical graph where functions become nodes and CALLS relationships become edges.
In src/store/store.c (lines 5609–5710), each function definition is registered as a vertex in the graph. Every direct or inferred call—including import-aware and type-inferred CALLS edges—creates an undirected weighted connection between nodes. The edge weight represents the frequency of interaction, with each call contributing approximately one unit (SKIP_ONE ≈ 1) to the total weight. Self-loops representing recursive calls are not stored as separate edges; instead, their weight is folded directly into the node's degree to maintain graph sparsity.
Deduplication and CSR Optimization
Before community detection begins, raw edges undergo deduplication and compression to enable fast traversal.
Compressing the Graph Structure
The louvain_build_weights() function aggregates duplicate edges between the same function pairs, summing their weights into a single value. The lg_build() routine then converts this edge list into a Compressed-Sparse-Row (CSR) representation (lines 5624–5750 in src/store/store.c). This compact data structure stores the graph in contiguous memory arrays, supporting constant-time neighborhood lookups essential for the modularity optimization passes.
The Leiden Refinement: Local Moving and Aggregation
The core detection logic implements a Leiden-refined Louvain strategy that iteratively refines community boundaries.
Optimizing Modularity Through Local Moves
The algorithm initializes a work-queue containing every node in the graph. For each node, it calculates the modularity gain of moving to each neighboring community. The node is shifted to the community yielding the highest positive gain. Critically, only neighbors of nodes that actually moved are re-queued for evaluation (lines 5572–5590 in src/store/store.c). This optimization drastically reduces computational work while guaranteeing convergence to a local modularity maximum.
Hierarchical Aggregation
Once the local-moving phase stabilizes (no further moves improve modularity), the algorithm aggregates each detected community into a single super-node. Edge weights between communities are summed to create a coarse-grained graph, and the process repeats. This hierarchical coarsening continues for up to LEIDEN_MAX_LEVELS = 64 iterations (lines 5722–5740 in src/store/store.c), uncovering multi-scale functional modules from fine-grained clusters to major architectural components.
Exposing Modules via the MCP Interface
The final community assignments are surfaced through the Model Context Protocol (MCP) server.
When the get_architecture tool is invoked with the "clusters" aspect, the cbm_store_get_architecture() function retrieves the Louvain-detected communities. In src/mcp/mcp.c (lines 3775–3804), these cluster IDs are packaged into the JSON response, mapping each function identifier to its assigned module ID. This enables downstream tools and UI visualizations to label and interact with functional modules as distinct architectural units.
Practical Usage: Querying Detected Modules
You can retrieve detected functional modules via the MCP tool interface, CLI, or Python client.
JSON-RPC Request
{
"tool": "get_architecture",
"params": {
"project": "my-project",
"aspects": ["clusters"]
}
}
Command Line Interface
codebase-memory-mcp get_architecture '{"project":"my-project","aspects":["clusters"]}'
Python Client
from codebase_memory_mcp import CodebaseMCP
client = CodebaseMCP()
arch = client.get_architecture(project="my-project", aspects=["clusters"])
for cluster_id, nodes in arch["clusters"].items():
print(f"Module {cluster_id}: {len(nodes)} functions")
The returned JSON contains a clusters object where keys represent community IDs and values are lists of function identifiers belonging to that specific functional module.
Summary
- Graph Representation: Functions become nodes and CALLS edges become weighted undirected connections in
src/store/store.c, with self-loops folded into node degrees. - Performance Optimization: The
louvain_build_weights()andlg_build()functions deduplicate edges and convert them to CSR format for fast neighborhood traversal. - Leiden Refinement: The algorithm uses a work-queue-driven local-moving phase that only re-evaluates neighbors of moved nodes, followed by hierarchical aggregation up to 64 levels.
- MCP Exposure: Final clusters are returned via
cbm_store_get_architecture()and exposed throughsrc/mcp/mcp.cwhen the"clusters"aspect is requested. - Multi-Scale Detection: The hierarchical approach reveals both fine-grained utility clusters and high-level architectural modules.
Frequently Asked Questions
What is the difference between the Louvain and Leiden algorithms in this implementation?
The codebase implements the Louvain algorithm enhanced with Leiden refinement strategies. While classic Louvain greedily optimizes modularity, the Leiden approach used here guarantees that nodes are only moved to communities that actually improve partition quality, and it uses a work-queue mechanism that re-queues only neighbors of nodes that changed communities. This produces more stable and higher-quality functional modules compared to the standard Louvain method.
How does the algorithm handle weighted versus unweighted call relationships?
The graph treats all relationships as weighted edges by default. Each CALLS edge increments the weight by approximately one unit (SKIP_ONE ≈ 1), meaning frequently called functions share stronger connections. The louvain_build_weights() function sums these weights during deduplication, and the modularity calculation uses these cumulative weights to determine community membership, ensuring that high-traffic call paths strongly influence module formation.
What is the maximum depth of hierarchical clustering supported?
The implementation supports up to 64 levels of hierarchical aggregation, defined by the LEIDEN_MAX_LEVELS constant. Each level coarsens the graph by collapsing detected communities into super-nodes. In practice, most codebases stabilize after 3–5 levels, but the 64-level limit accommodates extremely large or deeply nested architectures while preventing infinite recursion.
How are self-loops and recursive function calls treated in the graph?
Self-loops representing recursive calls are not stored as explicit edges in the sparse graph structure. Instead, their weight is folded into the node's degree during the initial construction phase (lines 5609–5710 in src/store/store.c). This design maintains memory efficiency and graph sparsity while still accounting for the internal complexity of recursive functions in the overall modularity calculation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →