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
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
getTimelineByQuery | src/services/worker/SearchManager.ts | 19.9 | 44 | 8 | 25 |
timeline | src/services/worker/SearchManager.ts | 19.6 | 56 | 7 | 31 |
runInstallCommandInner | src/npx-cli/commands/install.ts | 19.0 | 65 | 5 | 79 |
getContextTimeline | src/services/worker/SearchManager.ts | 18.7 | 32 | 7 | 26 |
search | src/services/worker/SearchManager.ts | 18.3 | 65 | 8 | 24 |
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
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.
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 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.
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 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.
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 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.
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 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.
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 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.
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:
| Pattern | Occurrences |
|---|---|
long_function | 6 |
complex_branching | 5 |
deeply_nested | 5 |
god_function | 5 |
exit_heavy | 4 |
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 →