claude-mem's SearchManager.ts carries the highest activity risk — 4 functions to address first

Four of claude-mem's five highest-risk functions live in a single file, SearchManager.ts, where cyclomatic complexity reaches 65 and nesting depth hits 8 — all actively changing as of the last two days.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk19.88Low
Hottest FunctiongetTimelineByQuery

Antipatterns Detected

long_function6complex_branching5deeply_nested5god_function5exit_heavy4

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in claude-mem?

A god function is one that takes on too many distinct responsibilities — making architectural decisions, constructing query filters, managing error state, formatting output, and calling into a broad set of collaborators, all without delegating to focused helpers. Fan-out — the count of distinct functions directly called — is one concrete measure: a fan-out above 15 is a strong signal, and `runInstallCommandInner` in claude-mem has a fan-out of 79, meaning a single change to that function can propagate side effects through 79 distinct call sites. In practice, god functions are hard to test in isolation (you need to satisfy all their dependencies simultaneously), hard to review in a diff (a small behavioral change can be buried in a large function body), and prone to cross-branch state bugs where a variable set in one path influences behavior in another. All five of the top hotspots in claude-mem are flagged with this pattern.

How do I reduce cyclomatic complexity in TypeScript?

The most effective first step is the extract-method refactoring: identify a coherent branch or sub-workflow inside the complex function — for example, the ChromaDB search path inside `search` in `SearchManager.ts` — and move it into a dedicated private method with a descriptive name. This reduces the original function's CC by the number of decision points in the extracted block, and it makes each path independently testable. A CC above 10 warrants splitting; above 30 it should be treated as a blocking refactoring item, and above 60 (as `timeline` and `search` both are, each at CC 65) it represents a function that is practically untestable at full path coverage. In TypeScript specifically, watch for implicit branching added by type narrowing — a `typeof x === 'string'` check inside an already-nested block adds a cyclomatic path that static analysis tools count, and conditional types in generic constraints can add paths that CC tools may undercount. For `getTimelineByQuery`, extracting the Chroma path and the FTS fallback into separate methods would cut the measured CC roughly in half.

Is claude-mem actively maintained?

Yes — every function in the repository falls into the `fire` or `watch` quadrant. The top five hotspots were all touched within the last three days: the four `SearchManager.ts` functions (`getTimelineByQuery`, `timeline`, `getContextTimeline`, and `search`) each have 1 touch in the last 30 days and were last modified 2 days ago, and `runInstallCommandInner` has 3 touches in the last 30 days and was modified zero days ago. Active maintenance and high structural complexity are not mutually exclusive — they are what makes the `fire` quadrant genuinely urgent rather than theoretical. The codebase is being developed at a pace where structural complexity in `SearchManager.ts` is being inherited by each new change, not resolved ahead of it.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. To reproduce this exact analysis, check out commit `f7f82e2` in the `thedotmack/claude-mem` repository and run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration — no `.hotspotsrc.json` is required to get started, though you can add one to exclude generated or vendored paths from the results.

What does activity-weighted risk mean?

Activity-weighted risk combines a structural complexity score — derived from cyclomatic complexity, nesting depth, and fan-out — with the function's recent commit frequency, so that functions which are both hard to understand AND actively changing score the highest. A function with cyclomatic complexity 80 that has not been touched in two years scores much lower than one with CC 20 that is modified every week, because the complex-but-dormant function has lower near-term regression probability. This prioritization helps focus refactoring effort where it reduces the chance of bugs being introduced right now — not just where the code looks complicated in the abstract. In claude-mem, all five top hotspots are in the `fire` quadrant, meaning structural complexity and active development are both present simultaneously, which is what makes them genuinely urgent.

Out of 2,292 functions analyzed in thedotmack/claude-mem at commit f7f82e2, 287 fall into the critical band — and four of the top five by activity-weighted risk are methods inside src/services/worker/SearchManager.ts, all touched within the last two days. The top-ranked function, getTimelineByQuery, carries a risk score of 19.88 with a cyclomatic complexity of 44 and a nesting depth of 8: it is both structurally dense and actively changing right now, which makes it a live regression risk rather than a backlog cleanup item. The fifth hotspot, runInstallCommandInner in src/npx-cli/commands/install.ts, has been touched 3 times in the last 30 days and was last modified zero days ago, adding a second active fire to the picture.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
getTimelineByQuerysrc/services/worker/SearchManager.ts19.944825
timelinesrc/services/worker/SearchManager.ts19.656731
runInstallCommandInnersrc/npx-cli/commands/install.ts19.065579
getContextTimelinesrc/services/worker/SearchManager.ts18.732726
searchsrc/services/worker/SearchManager.ts18.365824

Large Repo Analysis

claude-mem is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Quadrant overview

Triage Band Distribution
Fire794Watch1498

2,292 functions analyzed

Every function in this repo sits in either the fire or watch quadrant — there is no dormant complexity here. All 287 critical-band functions are in the fire quadrant, meaning structural complexity and active development are both present. That means every complex function is also an actively changing one.

Detected Antipatterns
Long Function×6Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Exit Heavy×4Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.

The antipattern picture across the top five is uniform: every hotspot is flagged as a god function and a long function, and all but one are deeply nested with complex branching. That consistency tells me these are not isolated outliers — they reflect a shared design pattern inside SearchManager.ts where multiple search strategies (semantic via ChromaDB, full-text via SQLite FTS, filter-only) are all resolved inside single monolithic async methods rather than delegated to focused handlers.


The SearchManager.ts cluster

Before looking at individual functions, it is worth stating plainly what it means that four of the top five hotspots share one file. Any change to SearchManager.ts has to be understood in the context of all four methods simultaneously. A developer fixing a bug in timeline is working in the same file as three other critical-band functions. The file-level external signals reinforce this: every SearchManager.ts entry shows 100% of its recorded commits tagged as bug fixes. That does not prove any current function is defective, but it does suggest this file has historically required correctional work, and that context should inform how carefully changes here are reviewed.


getTimelineByQuery — SearchManager.ts

getTimelineByQuery
src/services/worker/SearchManager.ts
19.88
fire
CC 44
ND 8
FO 25
touches/30d 1

getTimelineByQuery is the top-ranked function in the repository with an activity-weighted risk score of 19.88. Based on its name and the source excerpt, it handles query-driven timeline lookups — accepting a query string, an optional mode, depth parameters, and a project filter, then attempting to resolve results through two sequential search strategies: ChromaDB semantic search first, falling back to SQLite full-text search if Chroma is unavailable or returns nothing.

The cyclomatic complexity of 44 reflects what I can see directly in the excerpt: the function branches on whether chromaSync is active, then branches again on project filter presence to construct a $and-wrapped where clause, then filters results by a 90-day recency window, then checks result counts at multiple stages, then diverges on mode === 'interactive' to generate different output formats — each of those paths is an independent execution path that requires its own test. A CC of 44 means 44 such paths at minimum.

Cyclomatic Complexity 44
threshold: 10

The nesting depth of 8 is the more immediate readability concern. The excerpt shows try/catch blocks wrapping async Chroma calls nested inside if (this.chromaSync), which is itself inside the function body — by the time you reach the recency filter (chromaResults.ids.filter(...)) you are already five or six levels deep, and the mode === 'interactive' formatting block adds more. At ND 8, reasoning about which guard conditions apply to any given line of code requires tracking the full nesting stack mentally.

The fan-out of 25 means this function directly invokes 25 distinct callees — queryChroma, sessionStore.getObservationsByIds, sessionSearch.searchObservations, logging calls, and result-formatting logic are all happening in the same method body. That breadth means a change here can propagate to a wide surface of dependencies.

Recommendation: Extract the ChromaDB search path and the FTS fallback path into separate private methods — something like _searchChromaForTimelineQuery and _searchFtsForTimelineQuery — and reduce getTimelineByQuery to an orchestrator that calls them in sequence and handles the empty-results case. That alone would cut CC and FO significantly and make the fallback logic independently testable. The mode-specific formatting at the end of the function is also a natural extraction candidate.


timeline — SearchManager.ts

timeline
src/services/worker/SearchManager.ts
19.61
fire
CC 56
ND 7
FO 31
touches/30d 1

timeline has the highest cyclomatic complexity of the four SearchManager.ts hotspots at 56, and the highest fan-out in the file at 31. From the source excerpt, it handles two distinct operational modes: query-based timeline anchoring (find an observation matching a text query, use it as the temporal anchor) and direct anchor-based lookup (resolve a numeric or string anchor directly to a position in the timeline). The guard clauses at the top — returning early if neither anchor nor query is provided, and again if both are provided — account for several of those 56 paths before the main logic even begins.

Cyclomatic Complexity 56
threshold: 10
Fan-Out 31
threshold: 15

The fan-out of 31 is the broadest coupling footprint of any function in the top five. The excerpt shows calls to parseNumericAnchor, searchChromaForTimeline, sessionSearch.searchObservations, sessionStore.getObservationById, sessionStore.getTimelineAroundObservation, and logger methods — and that is before the function reaches its output-formatting phase. A change to any of those 31 callees has a direct path back to timeline’s behavior.

The function also carries the exit-heavy pattern: the multiple early returns for invalid input combinations, empty results, and missing anchor records each constitute an exit path that needs test coverage. Across 56 cyclomatic paths and 7 nesting levels, achieving meaningful test coverage for this function as written is a significant undertaking.

Recommendation: The query-mode and anchor-mode branches are logically independent workflows — they share parameter parsing at the top and output formatting at the bottom, but the resolution logic in between is entirely separate. I would split them into _resolveTimelineByQuery and _resolveTimelineByAnchor, leaving timeline as a thin dispatcher. This also makes the anchor-type branching (numeric ID vs. session ID string vs. ISO timestamp) more visible and independently testable — that type-narrowing chain is exactly the kind of subtle logic that TypeScript’s async/await and runtime type checks can make hard to reason about under test.


runInstallCommandInner — install.ts

runInstallCommandInner
src/npx-cli/commands/install.ts
19.04
fire
CC 65
ND 5
FO 79
touches/30d 3

runInstallCommandInner is the outlier in the top five: it lives outside SearchManager.ts entirely and has been touched 3 times in the last 30 days, with its most recent change zero days ago. That makes it the most actively moving function in this analysis.

Fan-Out 79
threshold: 15

The fan-out of 79 is the standout metric — it is more than three times the fan-out of any SearchManager.ts function. From the excerpt, this reflects the install command’s role as an orchestrator of the entire installation workflow: it reads plugin version, checks for existing installations, reads existing plugin JSON, presents interactive prompts (p.confirm, p.intro, p.cancel), detects installed IDEs (detectInstalledIDEs), handles non-TTY fallbacks, prompts for runtime and provider selection, conditionally prompts for Claude model configuration, and then proceeds into marketplace plugin registration and worker startup. Each of those responsibilities touches a different module, which is where the 79 distinct callees come from.

The cyclomatic complexity of 65 reflects the branching across interactive vs. non-interactive mode, already-installed vs. fresh install, IDE selection (explicit --ide flag, TTY interactive, non-TTY default), provider type, and the various cancel/error exits. The exit-heavy pattern is visible in the process.exit(0) and process.exit(1) calls scattered through the IDE validation branches.

The file-level external signals are the most informative of any hotspot in this analysis: a PR review comment density of 9.0 (the highest by far across the top five), 2 authors in the last 90 days, and one in three commits tagged as a bug fix across 3 total commits. The high review comment rate tells me that when changes to this file are proposed, they generate significant review discussion — which is consistent with a function this broad being hard to reason about in a diff context.

Recommendation: The IDE selection logic (flag-provided vs. TTY vs. default), the provider/runtime prompt sequence, and the marketplace registration steps are all coherent sub-workflows that could be extracted into dedicated async functions. I would start with the IDE selection block — it has its own validation, error exits, and fallback behavior, making it a natural extract-method candidate that would immediately reduce both CC and FO. Given the active commit churn and high review comment density, decomposing this function would also make future PRs easier to review because each change would be scoped to a smaller, named unit of behavior.


getContextTimeline — SearchManager.ts

getContextTimeline
src/services/worker/SearchManager.ts
18.67
fire
CC 32
ND 7
FO 26
touches/30d 1

getContextTimeline resolves a single anchor — which can arrive as a numeric observation ID, a session-prefixed string like S123 or #S42, or an ISO timestamp string — into a window of timeline context. The source excerpt makes the branching structure explicit: the outermost branch is a typeof anchor check (number vs. string vs. fallback error), and the string branch then splits further on whether the anchor starts with S or #S before falling through to date parsing.

Cyclomatic Complexity 32
threshold: 10

At CC 32 and ND 7, this is the structurally lightest of the four SearchManager.ts hotspots, but the nesting depth is still deep enough to warrant attention. The typeof dispatch at the outer level, then a string-prefix check, then a parseInt and session lookup, then a Date parse and isNaN validation — each layer adds both a path and a nesting level. The fan-out of 26 indicates that even after anchor resolution, the function calls broadly into the store layer (getObservationById, getSessionSummariesByIds, getTimelineAroundObservation, getTimelineAroundTimestamp) and then into timelineService.filterByDepth for depth-bounded filtering.

The exit-heavy pattern is visible in the excerpt: every branch that fails validation returns an error object inline rather than throwing or delegating to a central error handler, which means the error-response shape is defined in multiple places inside the same function body.

Recommendation: The anchor-type resolution logic — numeric ID, session string, ISO timestamp — is a well-defined parsing concern that could be extracted into a dedicated resolveAnchor(anchor: unknown): ResolvedAnchor | ErrorResponse helper. That would reduce getContextTimeline to: resolve the anchor, fetch the timeline window, filter by depth, format the output. Each of those is then independently testable, and the error-response construction is centralized.


search — SearchManager.ts

search
src/services/worker/SearchManager.ts
18.26
fire
CC 65
ND 8
FO 24
touches/30d 1

search ties timeline for the highest cyclomatic complexity in the top five at 65, and its nesting depth of 8 matches getTimelineByQuery for the deepest in the file. From the excerpt, it implements at least three distinct search paths: a filter-only path (no query text, go directly to SQLite), a ChromaDB semantic search path (query text present and Chroma available), and an implied FTS fallback path. Within the Chroma path, it branches on type to construct a doc_type where filter, then conditionally layers a project filter on top using a $or/$and construction, then handles date range logic — distinguishing between explicit date ranges and a default recency window — before hydrating results from SQLite.

Cyclomatic Complexity 65
threshold: 10
Max Nesting Depth 8
threshold: 4

The chromaFailed and chromaFailureReason state variables declared at the top of the function suggest that error state from one branch is carried forward and consulted in later branches — that kind of cross-branch mutable state inside a single function body is a common source of subtle bugs because the state’s meaning depends on which execution path set it. TypeScript’s type narrowing doesn’t help here since both variables are typed loosely (boolean and a nullable object literal).

The god-function pattern is apt: search is making decisions about which backend to use, constructing query filters, managing error state, handling recency windowing, and (presumably) formatting output — all without delegation. At fan-out 24, it is calling into a broad set of collaborators directly.

Recommendation: The three search paths (filter-only, Chroma semantic, FTS fallback) each deserve their own method. I would start by extracting the Chroma path — including the where-filter construction and the date-range logic — into a _executeChromaSearch method, because that is the densest branch and the one most likely to evolve as ChromaDB integration matures. Extracting it also isolates the chromaFailed state to a return value rather than a captured mutable variable, which removes an entire class of cross-branch state bugs.


Context functions worth monitoring

Outside the top five, several watch-quadrant functions are worth a brief note. workerHttpRequest in src/shared/worker-utils.ts has been touched twice in the last 30 days at low structural complexity — worth keeping an eye on as worker communication evolves. ensureServerStorageSchema in src/storage/sqlite/schema.ts was flagged as a long function despite low CC, which typically means a wide DDL block that is fine until schema migration complexity increases. Neither is a refactoring priority today, but both sit in infrastructure paths adjacent to the high-risk SearchManager.ts cluster.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
long_function6
complex_branching5
deeply_nested5
god_function5
exit_heavy4

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Reproduce This Analysis

git clone https://github.com/thedotmack/claude-mem
cd claude-mem
git checkout f7f82e2f177f02209cd2db326e023d526735d718
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Related Analyses