Leiden Communities vs PageRank for Finding Code Hotspots

repowise team9 min read
On this page

Leiden community detection and PageRank describe the shape of a dependency graph, and code hotspots come from somewhere else: change history. Leiden groups files into modules, PageRank ranks the files the rest of the graph leans on, and a hotspot is a file that keeps changing. Combine the three and you see where the risky code is and how far a change there reaches.

We run all three on every repository repowise indexes, so this post uses the real code and the real output from indexing the repowise repository itself (index at commit 215776c, 15 October 2026).

What does Leiden community detection tell you about a codebase?

Leiden finds the modules the code actually forms, as opposed to the ones the folder tree claims. It partitions the dependency graph so that files import each other more densely inside a group than across groups. The algorithm is from Traag, Waltman and van Eck, "From Louvain to Leiden: guaranteeing well-connected communities" (Scientific Reports 9, 5233, 2019). It fixes a known flaw in Louvain, which can return communities that are internally disconnected, and it guarantees connected ones.

In repowise, packages/core/src/repowise/core/analysis/communities.py runs Leiden through graspologic with a fixed random seed so two runs on the same commit give the same partition. A second pass re-partitions any community larger than 30% of the graph (with a floor of 20 files), so one giant cluster cannot hide the real modules inside it. Tests and examples are kept out of the clustering and attached afterward to the community of the production files they exercise, so test files do not bend the module boundaries.

The result on this repository is a list of communities, labeled from the paths inside them. These are the ten largest:

CommunityLabelFiles
0ui790
1cli409
2server374
3server/mcp_server294
4ingestion281
5analysis/health163
6api-client, vscode153
7alembic/versions123
8analysis, decisions100
9ui/graph96

Scroll the table sideways to see every column.

Output of get_overview(include=["graph"]) on the indexed repository.

Two rows show why this is more than a folder listing. Community 6 joins api-client and vscode, two packages with separate directories that depend on each other tightly enough to behave as one module. Community 8 pairs analysis with decisions. A reader learning the codebase gets a map of the real seams. Leiden gives the map; churn picks the files.

What does PageRank tell you about a dependency graph?

PageRank ranks a file by how much of the graph depends on it, weighted by how important the dependents are. It comes from Brin and Page, "The anatomy of a large-scale hypertextual Web search engine" (Computer Networks and ISDN Systems 30, 1998), where a page is important if important pages link to it. Substitute "imports" for "links to" and the same idea ranks files: a file imported by files that are themselves widely imported scores high.

In packages/core/src/repowise/core/ingestion/graph/_metrics.py, MetricsMixin.pagerank runs networkx.pagerank with the standard damping factor of 0.85 over a centrality subgraph that drops test-to-test edges, so test scaffolding cannot inflate the scores. The scores are cached for the build. A parallel symbol_pagerank does the same over the call and inheritance edges between functions and classes, with containment edges removed so the score reflects how code is used, not where it is declared.

These are the top production files by file-level PageRank on the repowise repository:

FilePageRankCommunity
packages/ui/src/lib/cn.ts0.006350
packages/core/src/repowise/core/persistence/models.py0.005542
packages/types/src/health.ts0.005470
packages/core/src/repowise/core/ingestion/languages/spec.py0.0047413
packages/core/src/repowise/core/ingestion/models.py0.004604
packages/api-client/src/types.ts0.004456

Scroll the table sideways to see every column.

Every one of them is a shared foundation: a class-name helper, the database models, a shared types module, the language spec. They are the files where a signature change reaches the most code. That is real information about blast radius, and it is a different question from where change happens: cn.ts is a small utility that rarely changes, so centrality sizes the reach and leaves the choice of files to history.

Why can't PageRank or Leiden find hotspots on their own?

Both are computed from a snapshot of imports, and a hotspot is a property of time. A file can be central and untouched for two years, or peripheral and rewritten every week. The graph cannot tell those apart because nothing in an import edge records how often the file changes or how often its changes turn out to be bug fixes.

The idea of a hotspot as churn multiplied by complexity is Adam Tornhill's, in Your Code as a Crime Scene (2015): the files worth attention are the ones that change often and are hard to change. Structure alone has no way to see the first half of that.

repowise measured the overlap directly. Take the 100 production files with the highest PageRank and ask how many are flagged as hotspots: 26. The other 74 are central and quiet. In the other direction, three of the most-changed files in the repository rank lower by centrality than the foundations above:

FileCommits (90 days)Bug-fix commits (~6 months)PageRank
ingestion/call_resolver.py82650.00030
analysis/dead_code/analyzer.py74660.00048
ingestion/parser.py77590.00317

Scroll the table sideways to see every column.

PageRank would not put call_resolver.py in a list of files to worry about. Its churn would.

How does repowise find code hotspots from change history?

It scores each file by exponentially decayed churn, ranks that score across the repository, and requires an absolute floor of recent activity. The code is in packages/core/src/repowise/core/ingestion/git_indexer/.

  1. Decayed churn. _temporal_hotspot_score in file_history.py sums, over a file's commits, the lines changed (added plus deleted, divided by 100 and capped at 3.0 per commit) times a weight that halves every 180 days (HOTSPOT_HALFLIFE_DAYS). One enormous reformat cannot dominate, and old churn fades.
  2. Percentile rank. compute_percentiles in enrich.py sorts files by that score, with 90-day commit count as the tiebreak, and assigns each a churn_percentile. The top quartile qualifies.
  3. Activity floors. meets_hotspot_floors also requires at least 3 commits in the last 90 days, plus either 8 or more commits or a decayed score of at least 0.5. On a quiet repository the top quartile would otherwise be every file touched at all.

On the repowise repository that flags 1,062 files. The five hottest source files by decayed score:

FileCommits (90d)Decayed churnChurn percentile
cli/commands/update_cmd/command.py7646.01.00
core/ingestion/call_resolver.py8245.71.00
core/analysis/dead_code/analyzer.py7437.01.00
server/mcp_server/tool_answer/answer.py5035.11.00
core/ingestion/parser.py7734.01.00

Scroll the table sideways to see every column.

The complexity half of Tornhill's formula enters through the code-health layer. get_health on call_resolver.py returns a file score of 4.52 out of 10, a maximum cyclomatic complexity of 17 in _resolve_free_call, and 2,244 lines of code, next to the history findings: 65 bug-fix commits in roughly six months and 90-day churn that rewrote 2.9 times the file's size. Across the repository the average health is 7.87 out of 10, and the average over hotspot files is 6.48. For a longer walkthrough of this signal, see git hotspot analysis for finding risky files.

How do the three signals combine?

Each signal answers a different question, and each is computed independently from a different input:

SignalInputAnswersDoes not answer
Leiden communitiesImport graphWhich files form a module?Which module is changing?
PageRankImport graphWhat does the rest of the code lean on?What changes often?
Hotspot (churn)Git historyWhere is change concentrated?How far does a change reach?

Scroll the table sideways to see every column.

The useful move is reading a hotspot against the other two. A hotspot that is also in the top PageRank band is the expensive case. On this repository persistence/models.py is one: 83 commits in 90 days and a PageRank of 0.00554, second highest in the graph. A change there fans out to the many files that import it, so it deserves review depth and tests. A hotspot like call_resolver.py has low centrality but sits at the top of the churn and bug-fix ranking, so it is a local, recurring cost: the right response is refactoring it, and the community tells you which module's owners should do it.

get_risk returns this reading for a path: hotspot score, dependents, co-change partners and the owner, so an agent or a reviewer sees churn and reach together. See change risk and the git intelligence layer for how it surfaces in practice, and how we build the dependency graph for what the graph contains.

Which signal should you trust for which decision?

Use communities to learn and to assign ownership, PageRank to size a blast radius, and churn to choose what to fix. Onboarding questions ("what are the modules?") belong to Leiden. Questions about breaking changes ("what depends on this?") belong to centrality. Questions about maintenance budget ("where do bugs keep appearing?") belong to history, and our 21-repository study of process versus structural metrics found co-change scatter, change entropy and ownership ranking above every complexity marker, with file size controlled.

How we checked

Every number above comes from indexing the repowise repository at commit 215776c on 15 October 2026. Community sizes and hotspot counts come from get_overview, per-file churn, bug-fix counts and the health score from get_risk and get_health, and the PageRank and overlap figures from the index database (graph_nodes and git_metadata tables), counting only non-test file nodes. The overlap is the number of the 100 highest-pagerank rows with node_type='file' and is_test=0 in graph_nodes whose file_path has is_hotspot=1 in git_metadata. Community detection uses graspologic Leiden, with NetworkX Louvain and directory grouping as fallbacks. The algorithm descriptions come from reading communities.py, _metrics.py, file_history.py and enrich.py in packages/core. To reproduce, run repowise init on a repository and query it with get_overview, get_risk and get_health.

FAQ

What is a code hotspot?
A code hotspot is a file that changes often and is costly to change. It is found from version-control history: recent, repeated churn ranked against the rest of the repository, usually read alongside complexity. In repowise, a file is flagged when it is in the top quarter by decayed churn and has at least 3 commits in the last 90 days.
Can PageRank find hotspots in a dependency graph?
PageRank finds central files, not hotspots. It scores how much of the graph depends on a file, which predicts blast radius. A central file that never changes has high PageRank and no hotspot signal. On the repowise repository only 26 of the 100 highest-PageRank production files are hotspots.
What is Leiden community detection used for in code?
It groups files into modules based on how densely they import each other, ignoring the folder layout. The result is a map of real module boundaries, useful for onboarding, ownership and architecture review. repowise runs Leiden with a fixed seed, so the same commit always gives the same communities.
What is the difference between Leiden and Louvain?
Leiden is a refinement of Louvain that guarantees every community is well connected. Louvain can produce communities that are internally disconnected. Leiden is also faster on large graphs. The details are in Traag, Waltman and van Eck (2019).
Does repowise include complexity in the hotspot flag?
The hotspot flag itself comes from decayed churn and activity floors. Complexity enters through the code-health layer, which scores each file on cyclomatic complexity, nesting and size alongside history signals like bug-fix count. get_health shows both for a file, and get_risk shows the hotspot score with dependents.
How is PageRank computed on a code graph?
Files are nodes and imports are directed edges, so rank flows toward the files that are imported. repowise uses networkx.pagerank with damping 0.85 over a graph without test-to-test edges, and a separate symbol-level version over call and inheritance edges.