career-ops' analysis layer carries the highest activity risk — 5 functions to address first

The analyze function in analyze-patterns.mjs carries a CC of 42 and fan-out of 54, making it the dominant structural risk in the career-ops codebase while still actively changing.

Stephen Collins ·
oss javascript refactoring code-health
Activity Risk16.55Low
Hottest Functionanalyze

Antipatterns Detected

god_function4long_function4complex_branching3exit_heavy3deeply_nested1

Key Points

What is a god function and why does it matter in career-ops?

A god function is one that attempts to do too much — it owns too many responsibilities, calls too many other functions directly, and accumulates branching logic that should be distributed across smaller, focused units. In concrete terms, fan-out measures the count of distinct functions directly called from a single function; when that number reaches the level seen in `analyze` in `analyze-patterns.mjs` — 54 distinct callees — it means a single function is the orchestration hub for a vast portion of the system. This makes it extremely hard to test in isolation, because any unit test must account for the behavior of dozens of collaborators. It also means that a change anywhere in that call graph — or in `analyze` itself — can produce unexpected ripple effects that span the majority of the codebase. career-ops has 4 functions flagged with the god function pattern, with `analyze` being the most severe example.

How do I reduce cyclomatic complexity in JavaScript?

The most direct technique is extract-method refactoring: identify coherent clusters of branching logic within a large function and pull them into well-named helper functions, each handling a single decision or transformation. A cyclomatic complexity above 15 is a reasonable threshold to begin splitting; above 30 — as is the case with `analyze` at CC 42 in `analyze-patterns.mjs` — warrants immediate decomposition because the number of required test cases grows proportionally with every new branch. A practical first step today is to identify the largest `if/else` or `switch` block inside `analyze` and extract it into a named function that can be tested independently; this alone is likely to cut CC by a third or more. Once the function is decomposed, each extracted piece becomes individually testable, and the remaining orchestration logic in `analyze` becomes easier to reason about.

Is career-ops actively maintained?

Yes — the data strongly suggests active, ongoing development. Every one of the 187 analyzed functions has enough recent activity to rank for monitoring or refactoring, rather than sitting as dormant complexity. The top hotspot, `analyze` in `analyze-patterns.mjs`, was touched within the last 27 days, and `handleKey` in the pipeline dashboard screen was touched 5 times in the last 30 days alone. Active development and structural complexity are not mutually exclusive — in fact, career-ops is a case where the two are occurring simultaneously across its most critical functions, which is precisely what makes the activity-weighted risk scores meaningful rather than theoretical.

How do I reproduce this analysis?

The analysis was produced using the Hotspots CLI, available at github.com/hotspots-dev/hotspots, against commit `8e554cc` of the `santifer/career-ops` repository. After running `git checkout 8e554cc` in a local clone of the repo, execute `hotspots analyze . --mode snapshot --explain-patterns --force` to reproduce the exact results. The same command works on any local git repository without additional configuration, so you can run it against your own codebase immediately.

What does activity-weighted risk mean?

Activity-weighted risk multiplies structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — by a measure of 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 touched every week, because the complex-but-dormant function presents lower near-term regression risk. This prioritization helps teams focus refactoring effort where it reduces the probability of bugs being introduced right now, not just where the code looks complicated in the abstract. In career-ops, `analyze` scores at the top precisely because its structural complexity of CC 42 and fan-out of 54 are paired with recent commit activity — that combination, not complexity alone, is what makes it the highest priority.

career-ops is a JavaScript pipeline tool with 187 analyzed functions, 33 of which rank as high-priority refactoring candidates. The top hotspot, analyze in analyze-patterns.mjs, carries a cyclomatic complexity of 42, a fan-out of 54, and enough recent change activity to make it more than a cleanup backlog item. Notably, every function in the codebase has recent activity behind it, which means the structural risk in this repo is being exercised by ongoing development rather than sitting dormant.

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
analyzeanalyze-patterns.mjs16.642454
handleKeydashboard/internal/ui/screens/pipeline.go13.52138
Updatedashboard/main.go13.118218
parseTsvContentmerge-tracker.mjs12.921411
applyupdate-system.mjs12.734518

Hotspot Analysis

analyze — analyze-patterns.mjs

Based on its name and file path, analyze is the core pattern-analysis entry point for the career-ops pipeline — the function responsible for driving whatever structural or behavioral analysis the tool performs. Its cyclomatic complexity of 42 means there are at least 42 independent execution paths to reason about and test; its nesting depth of 4 shows meaningful layered branching; and its fan-out of 54 means it directly invokes 54 distinct functions, making it the broadest coupling point in the codebase by a wide margin. Because it is still changing, any update to this god function can affect a wide swath of downstream behavior with little structural guidance on where the failure originated.

Recommendation: Apply extract-method refactoring to decompose analyze into focused sub-functions, each responsible for a single phase of analysis — this alone should cut both CC and fan-out substantially. Before refactoring, add characterization tests that cover the most common execution paths to create a safety net against regressions.

handleKey — dashboard/internal/ui/screens/pipeline.go

From its name and location in the dashboard’s pipeline screen, handleKey almost certainly dispatches keyboard input events within the pipeline UI — a function that branches on every supported key action. Its cyclomatic complexity of 21 reflects the breadth of that dispatch logic, while its nesting depth of 3 and fan-out of 8 suggest a moderate number of helper calls behind those branches. The exit-heavy pattern flag signals multiple return paths, each of which represents a distinct test case that must be covered to prevent silent regressions as new key bindings are added.

Recommendation: Introduce a dispatch table or command pattern to replace the branching key-handling logic, which would reduce CC significantly and make adding new key bindings a data change rather than a structural one. Prioritize this now — with 5 touches in 30 days, the probability of a regression from the next change is high.

Update — dashboard/main.go

In a dashboard entry point, Update is likely the central state-transition function that routes incoming messages and coordinates screen updates. Its cyclomatic complexity of 18 is lower than the top two hotspots but still high enough to make regression testing important, and its fan-out of 18 indicates broad coordination across helpers or view state. The nesting depth of 2 is manageable, so the main risk is breadth rather than deeply layered control flow.

Recommendation: Split the highest-volume message handling paths into focused update helpers and keep Update as a thin router. This should lower CC while preserving the function’s role as the dashboard’s coordination point.

parseTsvContent — merge-tracker.mjs

parseTsvContent appears to turn tab-separated merge-tracking data into structured records. Its cyclomatic complexity of 21 and nesting depth of 4 point to multiple validation and normalization paths inside the parser, while a fan-out of 11 suggests it is also coordinating several helper operations. That combination makes malformed or edge-case input the most likely source of regressions.

Recommendation: Separate tokenization, row validation, and record construction into independently tested helpers. Add fixtures for empty rows, missing columns, escaped delimiters, and invalid values before changing the parser’s control flow.

apply — update-system.mjs

apply is the fifth-ranked hotspot and has the most deeply nested control flow in the table: CC 34, ND 5, and FO 18. That profile is consistent with a long, branching update routine that performs several decisions before delegating to downstream operations. The deeply nested and complex-branching patterns are plausible here because a reader has to track both many paths and several levels of conditional context.

Recommendation: Extract the deepest conditional branches first, especially any validation, planning, or side-effect steps that can become named helpers. Once the branches are isolated, add focused tests around each update scenario so the orchestration in apply can shrink without changing behavior.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
god_function4
long_function4
complex_branching3
exit_heavy3
deeply_nested1

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.

Key Takeaways

  • The analyze function in analyze-patterns.mjs has a fan-out of 54 — the single highest coupling point in the codebase. Any engineer touching it should map its downstream call graph before making changes, as a regression here can propagate across more than half the codebase’s function surface.
  • handleKey in dashboard/internal/ui/screens/pipeline.go was touched 5 times in the last 30 days with a cyclomatic complexity of 21 and multiple exit paths — introduce a command dispatch table before the next feature addition to prevent CC from climbing further.
  • The remaining top-five functions — Update, parseTsvContent, and apply — all combine double-digit CC with active change, so they should get characterization tests before structural refactoring begins.

Reproduce This Analysis

git clone https://github.com/santifer/career-ops
cd career-ops
git checkout 8e554cc4437c3a58e813378abb9b35e2e08a007e
hotspots analyze . --mode snapshot --explain-patterns --force

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

Hotspots highlights structural and activity risk — not “bad code.” Findings are a prioritization aid, not a bug predictor. Editorial policy →

Run this on your own codebase

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

macOS
$ brew install Stephen-Collins-tech/tap/hotspots
Linux / cargo
$ cargo install hotspots-cli
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Related Analyses