# How the Louvain Algorithm Detects Functional Modules in Codebase-Memory-MCP

> Discover how the Louvain algorithm detects functional modules in your codebase. Learn about its C implementation for optimized call-graph clustering and modularity.

- Repository: [Martin Vogel/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)
- Tags: deep-dive
- Published: 2026-07-17

---

**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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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

```json
{
  "tool": "get_architecture",
  "params": {
    "project": "my-project",
    "aspects": ["clusters"]
  }
}

```

### Command Line Interface

```bash
codebase-memory-mcp get_architecture '{"project":"my-project","aspects":["clusters"]}'

```

### Python Client

```python
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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/store/store.c), with self-loops folded into node degrees.
- **Performance Optimization**: The `louvain_build_weights()` and `lg_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 through [`src/mcp/mcp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/mcp/mcp.c) when 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`](https://github.com/DeusData/codebase-memory-mcp/blob/main/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.