# Architectural Hotspots

> Surfaces architectural hotspots in a codebase — high-coupling hub files, tangled high-fan-out modules, oversized god modules, and dependency cycles. Use this skill when the user asks where to refactor, which files are architecturally problematic, which modules are most tightly coupled, where the tech debt is concentrated, what should be split up, or how the dependency structure of a project looks. Also trigger on phrases like "find hotspots in this repo", "coupling problems", "god classes", "circular dependencies", "refactor candidates", or "which files do too much" — even if the user does not say the word "architecture". Do NOT trigger for runtime *performance* hotspots — "why is my app slow", "profile this", CPU/memory bottlenecks — this skill sees structure, not execution; for static perf findings use `code-audit-deep`, for real measurements suggest a profiler. This skill is **scope: whole repository**. If the user is pointing at a specific file or asking what is wrong *inside* a file ("hotspots in commit.

- Skill: `stoica-mihai/architectural-hotspots` (Agent Skill, multi-file: 21 files)
- Install (CLI): `npx skillmds@latest add stoica-mihai/architectural-hotspots`
- Raw SKILL.md: https://api.skillmd.com/api/skills/stoica-mihai/architectural-hotspots/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: Stoica-Mihai (https://skillmd.com/u/stoica-mihai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/stoica-mihai/architectural-hotspots

---


# Architectural Hotspots

Static dependency-graph analyzer that ranks files along five orthogonal
dimensions instead of inventing a single composite score:

- **Hubs** — many other files depend on them (high fan-in).
- **Tangles** — they depend on many other files (high fan-out).
- **God modules** — large files concentrating too much responsibility.
- **Churn** — files most frequently touched by commits (git repos only).
- **Cycles** — strongly-connected components of size > 1 in the import graph.

Each dimension answers a different question; the human reader combines them.
A file showing up in **two** sections is more interesting than one with a high
score in any single dimension.

## When to run this

Run it when the user is asking a *strategic* question about the codebase —
"where should I start refactoring", "what's tightly coupled", "is this app
architecturally sound", "which files keep biting us". Do not run it for
narrow questions about a specific file or function.

Skip it on:
- Single-file edits or bugfix tasks.
- Codebases with fewer than ~30 source files — the analyzer needs scale to
  be useful, and on small repos the user can read the structure directly.
- Repos that are mostly generated code or templates.

## How to run

The analyzer is a single Python 3 script with no third-party dependencies.

```bash
python plugins/architectural-analysis/skills/architectural-hotspots/scripts/find_hotspots.py <repo_path>
```

Useful flags:

- `--top N` — rows per section (default 15).
- `--god-loc N` — LOC threshold for the god-modules section (default 400).
- `--churn-months N` — window for the git-churn section (default 12;
  0 disables it). Widen on slow-moving repos, narrow on very active ones.
- `--output report.md` — write to file instead of stdout.
- `--exclude GLOB` (repeatable) — skip files matching a glob against the
  repo-relative path. Matches at any depth, so `--exclude '*_tests.rs'` and
  `--exclude 'design/probes/**'` both work. Reach for it when the user scopes
  the request ("everything except the probes") and when test files crowd the
  god-modules table.
- `--explain` — append a per-import trace: source file, raw import token, what
  it resolved to, and whether it counted as a dependency, a declaration, or was
  dropped. **Run this whenever a number looks wrong.** A surprising fan-in or a
  cycle you can't explain is nearly always a resolution artifact, and this flag
  is the only way to see it without reading the analyzer's source.
- `--ext .EXT=LANG` (repeatable) — force a file extension to be parsed as a
  specific language. `LANG` can be any precise tag (`python`, `js`, `go`,
  `rust`, `java`, `c`, `ruby`, `csharp`, `php`, `swift`, `scala`, `qml`,
  `nix`) or `generic`. Use when a project uses unusual extensions
  (`--ext .nut=generic` for Squirrel scripts).
- `--sniff` — parse files with unknown extensions if they look like text.
  Catches obscure languages at the cost of more false positives. Off by
  default; turn on for polyglot or unusual repos.

Two test scripts live next to the analyzer. Run both after any change to import
patterns, resolution, or the graph build:

```bash
python3 plugins/architectural-analysis/skills/architectural-hotspots/scripts/test_resolver.py
python3 plugins/architectural-analysis/skills/architectural-hotspots/scripts/test_mutations.py
```

`test_resolver.py` builds synthetic Rust, Python and Go repos whose correct
graphs are known and asserts the analyzer agrees. Four resolver defects shipped
together precisely because the language with the most special-case machinery
had no fixture.

`test_mutations.py` disables each fix in turn and checks that exactly the
declared assertions go red. It exists because an assertion that has only ever
been green is not known to work — and because the failure that kept recurring
here was not a wrong answer but *a check passing for the wrong reason*: a
one-sided cycle assertion, a mutation whose filter matched nothing, and defects
every existing eval was structurally blind to. Comparing against an exact set
catches both an assertion gone inert and a fix that has grown an unintended
dependency.

The script enumerates files via `git ls-files` when the target is a git repo
(so `.gitignore` is honoured for free), and falls back to `os.walk` with a
hardcoded ignore list (`node_modules`, `.venv`, `target`, `dist`, …) otherwise.

**Precise parsers** (per-language regex, lower false-positive rate):
Python, JS/TS, Vue, Svelte, Rust, Go, Java/Kotlin, C/C++, Ruby, C#, PHP,
Swift, Scala, QML, Nix.

**Generic fallback** (best-effort regex on common import keywords —
`import`, `require`, `use`, `include`, `from`, `open`, `with`, `mod`,
`extern crate`): Dart, Elixir/Erlang, Haskell, OCaml, F#, Lua, R, Julia,
Nim, Zig, V, Crystal, Perl, shell, Clojure, Lisp, Scheme, Racket, Ada,
Pascal, D, Groovy, GDScript, Gleam, Solidity, Move, Pkl, Elm. Lossier than
the precise parsers — extra false positives possible — but means the
analyzer degrades gracefully on language families without dedicated
support. For everything else: see the fallback clause below.

Imports are resolved to internal repo files by:

1. **Path-alias expansion** for TypeScript / JavaScript projects — the
   analyzer reads `tsconfig.json` / `jsconfig.json` and resolves aliases like
   `@/lib/foo` according to the project's declared `paths`.
2. **Relative path resolution** where the language supports it (`./`, `../`,
   leading-dot Python imports; Rust `crate::`/`self::`/`super::` virtual roots
   and `mod foo;` declarations — including `#[path = "..."]` — resolve by
   Rust's module-file conventions, never by name matching).
3. **Basename match across the repo** otherwise — the last segment of the
   import path is matched against file stems, with ties broken by directory
   proximity to the source file.

For files **without a recognised extension** the analyzer additionally tries:
- The first line as a shebang (`#!/usr/bin/env python3` → Python).
- A Vim / Emacs modeline (`# vim: ft=lua`).
- With `--sniff`, treat the file as generic if it looks like text.

External, third-party, and unresolved imports are dropped. **The graph only
reflects intra-repo coupling.**

### When the analyzer can't help

If the report says **"Analyzed 0 source files"** or shows a non-empty repo
producing **0 internal import edges**, the codebase is in a language the
analyzer doesn't parse, or the language uses a coupling mechanism the
analyzer can't see (component-instantiation in QML, runtime DI in some Java
projects, codegen in others). Do not pretend the report is the answer in
that case. Say so explicitly to the user, fall back to reading the codebase
manually (or with grep/LSP), and offer to extend the analyzer if this
language family will come up again. A blank report with no caveat is worse
than no report at all — it implies the codebase is healthy when really the
tool just didn't see it.

### Sanity-check the ranking before you believe it

Total failure is easy to spot. **Partial garbage is the common case and looks
exactly like a real finding**, so run these four checks on every report before
you write a word of interpretation. Each one takes seconds and each has caught
a shipped defect:

1. **Is the top tangle a file whose LOC is roughly its fan-out?** A 22-line
   file with fan-out 22 is a list of declarations, not a coordination layer.
2. **Does a hub's fan-in survive `--explain`?** Spot-check the highest fan-in
   file. If its inbound edges are standard-library paths whose segments happen
   to match a directory name (`std::os::unix::fs` binding to your `fs/`), the
   ranking is measuring your directory names.
3. **Is a reported cycle made of real value-level dependencies?** Open both
   files and find the two imports. Type-only imports, containment declarations,
   and test modules that compile into their parent are not cycles.
4. **Are the god-module rows test files?** The table marks them. Test files are
   supposed to be big; re-run with `--exclude` to rank production code.

If a check fails, say so to the user, re-run with `--explain` or `--exclude`,
and only then interpret. Presenting an artifact as a finding costs the user a
refactor; saying "the tool got this wrong" costs a sentence.

## Reading the output

The report is markdown with seven sections in this order:

### Hubs

Sorted by fan-in (descending). High fan-in is *not automatically a problem* —
core libraries, type modules, and shared clients are *supposed* to be hubs.

A hub becomes a refactor candidate when **at least one** of the following is
also true:

- Its name is generic / grab-bag (`utils.py`, `helpers.ts`, `common.go`,
  `misc.rs`). That naming usually means the file accumulated unrelated
  responsibilities because nobody knew where else to put them.
- It also appears in the god-modules table (large + many dependents = the
  blast radius of changes is huge).
- It also ranks high in the Churn table, or the user mentions it changes
  often, breaks often, or is constantly in merge conflicts — observable
  symptoms of an over-loaded hub.

If a hub is small, focused, and well-named (e.g. `db/client.py` at 80 LOC),
note it as healthy and move on.

### Tangles

Sorted by fan-out (descending). High fan-out means the file reaches into
many parts of the repo — a sign of either:

- **Coordination / glue code** doing too much orchestration (a god service,
  a single huge handler).
- **Wrong layer** — a low-level module reaching up into application code, or
  a view layer reaching directly into data layer internals instead of going
  through a seam.

A fan-out of 2–3 in a small file is usually fine. The interesting cases are
files with fan-out in the double digits, especially when LOC is also high.

Declaration edges are excluded here. A Rust `mod x;` line says x lives *inside*
this file, which is containment, not dependency — counting it made every
`mod.rs` the top tangle. If a file's fan-out still looks implausible next to
its size, that is check 1 in the sanity list above.

### God modules

Files whose LOC is at or above the threshold. Cross-reference these against
the Hubs and Tangles tables: a file that is both **god-sized and a hub** is a
top refactor priority because every change to it ripples widely.

The **Test** column marks files the analyzer believes are tests (by directory
name or filename convention). Test files are *supposed* to be large, so they
tell you nothing about production structure. When they crowd the table, re-run
with `--exclude` and rank the real code.

The table shows two size columns: **LOC** (non-blank lines, comments
included — the `--god-loc` threshold applies to this one) and **Code**
(line-leading comments stripped; the table is ranked by this one, so a
400-line file that is 60% comments no longer outranks a dense 400-line
file). The comment stripping is a crude line-prefix heuristic, not a
parser — treat both as rough size signals.

### Churn

Commit-touch counts from git history over the configured window (default 12
months). Skipped with an explicit note when the target is not a git repo.

Churn is a *multiplier*, not a problem by itself — high churn on a small,
well-factored file just means active development. The signal is the
combination (this is the CodeScene-style hotspot insight: bug density
concentrates in files that are both complex and frequently changed):

- **Churn + god-size** — complexity the team pays for on every change.
  Highest-priority refactor target in the whole report.
- **Churn + hub** — every change risks rippling to all dependents; expect
  frequent breakage and merge conflicts.
- **God-size, low churn** — large but stable. Usually fine to leave alone;
  refactoring it has little payoff.

Use churn to *order* the candidates the structural tables surfaced: between
two god modules, fix the high-churn one first.

Caveats: churn counts commits, not diff size; a rename resets a file's
history; bulk reformat or license-header commits inflate everything they
touch.

### Cycles

Files inside a strongly-connected component cannot be understood, tested, or
deployed in isolation — they all need each other. Cycle size matters:

- **Size 2** — often a missing seam. Two modules import each other because
  there is a shared concept that wants its own home. Extract a third module
  both depend on.
- **Size 3+** — usually a layering violation. Some abstraction has been
  inverted: a "lower" layer is reaching back into a "higher" one. Identify
  the seam where the cycle crosses a conceptual boundary and break it there
  (dependency inversion, callbacks, events).

Cycles make the strongest and most expensive claim in the report, so verify
every one before repeating it. Open both files and find the two imports. A
false positive in Hubs costs a second look; a false positive here costs a
refactor of an architecture that was already correct. Worse, a phantom cycle
*swallows* real ones — a spurious 7-file component hides a genuine 2-file
cycle inside it, and the reader correctly dismisses the whole thing as too big
to act on.

### Cross-reference

Lists every file appearing in two or more of the other tables, ranked by how
many. This is the section to act on: the individual tables are single
dimensions, and a file that is simultaneously god-sized, high-churn, and a hub
is a far stronger signal than the top row of any one table. Read the other
tables for the evidence behind a row here.

### Limitations

The output already lists these, but worth repeating because they shape how
much weight to give the report:

- Path-alias indirection (`@/components/...` in Next.js, `~/lib/...` in some
  TS configs), dynamic imports, re-exports through barrel files, and
  codegen targets can all cause the resolver to miss real edges.
- TypeScript type-only imports (`import type ... from`, `export type ...
  from`) are **excluded** from the graph — they are erased at compile time,
  so they are not runtime dependency edges and must not create cycles.
  Inline type specifiers (`import { type X } from ...`) are still counted
  even when every specifier is type-only, so a rare false edge of that
  shape remains possible.
- Basename-match resolution can produce false positives when two unrelated
  files share a stem. The proximity tie-breaker helps but is not perfect.
- **External imports whose first path segment matches a project directory
  name** can bind to a repo file. Language standard libraries and crates
  declared in `Cargo.toml` are filtered out, but an undeclared dependency —
  or a language whose stdlib the analyzer doesn't enumerate (JS, C, and the
  generic fallback set) — is not. The colliding names are exactly what people
  call directories: `fs`, `os`, `io`, `net`, `sync`, `time`, `path`.
- **A directory-name match is a guess about which file inside is meant.** It
  resolves to the module root (`mod.rs`, `__init__.py`, `index.*`) when one
  exists, and otherwise picks the nearest file in the directory, which may be
  the wrong one.
- Rust `mod x;` declarations and `#[cfg(test)]` child modules are excluded —
  the first is containment rather than dependency, the second compiles into
  its parent. Both used to manufacture cycles.
- The graph is file-level. A god-class buried inside a moderate-sized file
  is invisible to this analysis.

State these caveats in your summary to the user so the report is taken as a
starting point for human judgement, not an authoritative verdict. **Stating a
caveat is not a substitute for the sanity checks above** — a reader who is
told "basename matching is imperfect" and then handed a phantom cycle will act
on the cycle.

## Communicating findings

After running the script, do not just paste the table. The report is the
*evidence*; the user wants the *interpretation*.

A good response looks like:

> Ran the hotspot analyzer on `src/`. Three files are worth your attention:
>
> 1. **`src/utils/helpers.ts`** (LOC 312, fan-in 47) — both a god module and
>    a hub. Generic name + accumulated dependents = high blast radius. Worth
>    splitting by domain.
> 2. **`src/services/order_service.py`** (LOC 612, fan-out 18) — large
>    coordination layer reaching into many modules. Candidate for breaking
>    into orchestrator + steps.
> 3. **One cycle of 3 files** between `models/user.py`, `models/order.py`,
>    `models/audit.py` — likely a shared concept (audit context?) wanting
>    its own module.
>
> Full report at `<path>` if you want the raw numbers. Caveat: the resolver
> uses basename matching, so path-aliased imports may be undercounted.

Always include:

1. The 2–4 files that stand out, with the *reason* they stand out (which
   tables they hit, what the combination implies).
2. One concrete suggestion per finding — even if hedged. "Split by domain",
   "extract a shared module", "invert the dependency".
3. The caveat about resolution limits, so the user calibrates trust.

Do not dump the entire table unless the user asks for it. The script writes
markdown that *can* be shown verbatim — but lead with the human-readable
summary first.

## Chain to code-audit-deep

File rankings are *evidence of where to look*, not findings the user can act
on directly. "Refactor `commit.rs`" is not actionable; "`commit.rs:426` —
`SystemTime::now()` called per nonce, hoist to constructor" is.

**Validate the ranking before you chain.** Run the four sanity checks from
"Sanity-check the ranking before you believe it". Auditing a 22-line
declaration file and a 55-line `main.rs` because they topped a broken tangle
table burns a full audit pass and returns nothing — and the audit's silence
then reads as "this code is fine" rather than "we audited the wrong files."
If a check fails, fix the run (`--explain`, `--exclude`) and re-rank first.

After producing the file-level summary above, **if the user asked the broad
"what are the hotspots?" question (or any equivalent open-ended hotspots
question that didn't name a specific file), do not stop at the ranking**.
Continue:

1. Pick the **top 3-5 files** from the Cross-reference table — it already
   ranks by combined signal (god + hub, god + tangle, high churn + any
   structural table, files in cycles). Skip rows marked as test files unless
   the user asked about test structure.
2. Invoke the `code-audit-deep` skill on those files. That skill reads the
   files end-to-end and emits categorized line-level findings (perf,
   correctness, durability, memory, complexity, coupling).
3. Present both layers in the final response — rankings first ("here is the
   shape of the problem"), then line-level findings per top file ("here is
   what is wrong inside them, with line numbers and fix sketches").

Skip the chain step **only** when:
- The user explicitly asked for rankings only ("just the file list", "no
  details, just rank them").
- The repo is too small for hotspots to be meaningful (< ~30 files) — in
  that case run `code-audit-deep` directly on the whole tree's source
  files instead of going through this skill.
- The user named specific files up front. Then `code-audit-deep` should
  have been picked instead of this skill from the start.

The reason for chaining: this skill's iter-0 failure mode (observed by the
author) was producing only file rankings when the user actually wanted the
line-level findings. The user's natural phrasing for *what's broken in
this codebase* is "what are the hotspots?" — and that question is answered
properly only when both layers are presented together.

## When the analyzer disagrees with the user

If the user pushes back ("`utils.ts` is fine, leave it alone") trust them —
they know the codebase. The analyzer surfaces *candidates*; humans confirm.
Update your reading of the codebase based on their pushback rather than
re-arguing from the metrics.

