novuhq/novu's framework layer carries the highest activity risk — 2 functions to address first

Two vendored functions bundled inside packages/framework/src/jsonSchemaFaker.js dominate novu's critical-band scores with cyclomatic complexity values of 257 and 468, flagging the framework package's dependency hygiene as the most urgent structural concern.

Stephen Collins ·
oss typescript refactoring code-health
Activity Risk21.23Low
Hottest Functionnode_modules/yaml/dist/resolveSeq-d03cb037.js

Antipatterns Detected

complex_branching5deeply_nested5exit_heavy5god_function4long_function4hub_function1

Key Points

What is a god function and why does it matter in novu?

A god function is one that calls into an unusually large number of distinct other functions, accumulates broad conditional logic, and effectively owns too much responsibility to be reasoned about or tested in isolation. In novu, the clearest example is the `src/shared.js` function inside `packages/framework/src/jsonSchemaFaker.js`, which has a fan-out of 382 — meaning it directly calls 382 distinct locations. A change anywhere in that call graph can produce unexpected side effects that trace back through this single function. The practical consequence is that testing it comprehensively requires constructing an enormous number of scenarios, and a regression introduced here can manifest in any of those 382 downstream paths.

How do I reduce cyclomatic complexity in TypeScript?

The most effective first step is the extract-method refactoring: identify the largest independent conditional branch inside the complex function and move it into a named function with a clear single responsibility. A cyclomatic complexity above 15 is a reasonable threshold to start splitting; above 30 it warrants immediate attention, and the two critical functions in novu's jsonSchemaFaker.js — at CC 257 and CC 468 — are well beyond any actionable threshold for manual refactoring, which is why replacing the inlined vendor code with a proper package dependency is the right move rather than attempting to decompose the functions line by line. For application-owned TypeScript code, replacing deeply nested conditionals with early-return guard clauses and decomposing switch statements into strategy-pattern objects are the two techniques that most reliably cut CC in half with a single refactoring pass.

Is novu actively maintained?

The quadrant data points to active development: the two critical-band functions at the top of the list are in the fire quadrant, meaning they have been touched within the last 30 days. The concentration of critical-band scores in a single file (jsonSchemaFaker.js) is a packaging issue rather than a maintenance gap — those are vendored library internals, not dormant application code. Active development and structural complexity coexisting in the same codebase is normal at this scale: 15,181 functions with 922 in the critical band is a meaningful surface area, but the fire-quadrant activity on the framework package and the agentic wizard path suggests the team is engaged with both areas.

How do I reproduce this analysis?

The Hotspots CLI is available at https://github.com/hotspots-dev/hotspots. This analysis was run against commit `60720ba` of novuhq/novu. After running `git checkout 60720ba` in your local clone, execute `hotspots analyze . --mode snapshot --explain-patterns --force` to reproduce the results — the same command works on any local git repository without additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from its cyclomatic complexity, maximum nesting depth, and fan-out — with how frequently it has been touched by recent commits. A function with a cyclomatic complexity of 200 that hasn't been changed in two years scores considerably lower than one with a cyclomatic complexity of 20 that has been modified ten times in the past month, because the actively-changing function presents a live regression surface right now. The intent is to help teams distinguish between code that is structurally messy but stable (lower urgency) and code that is both hard to reason about and being actively edited (higher urgency), so refactoring effort gets directed where it is most likely to prevent bugs from being introduced in the near term.

Across 15,181 functions in novuhq/novu, 922 land in the critical band — and the top two by activity-weighted risk score (21.23 each) sit inside a single file: packages/framework/src/jsonSchemaFaker.js. Both are vendored functions whose structural numbers are extreme by any measure, and both were touched within the last 30 days. I would start there not because the library code itself needs rewriting, but because the way those functions are surfacing in first-party analysis signals a packaging problem that can obscure real risk elsewhere in the repo.

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
node_modules/yaml/dist/resolveSeq-d03cb037.jspackages/framework/src/jsonSchemaFaker.js21.22579146
src/shared.jspackages/framework/src/jsonSchemaFaker.js21.24689382
processSdkMessagepackages/novu/src/commands/wizard/pipeline/steps/run-agent.ts18.949636
objectTypepackages/framework/src/jsonSchemaFaker.js18.670547
detectPackageManagerTypepackages/add-inbox/src/config/package-manager.ts18.42588

Large Repo Analysis

novu 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.

Codemod / Tooling Files in Results

Both of the top-ranked functions — node_modules/yaml/dist/resolveSeq-d03cb037.js and src/shared.js — are third-party library internals that have been bundled or inlined into packages/framework/src/jsonSchemaFaker.js. Their extreme complexity metrics (CC 257 and CC 468 respectively) reflect the full internal logic of the yaml and json-schema-faker libraries, not application code written by the novu team. To exclude this file from future analyses, add the following to your .hotspotsrc.json: { "exclude": ["packages/framework/src/jsonSchemaFaker.js"] }. This will prevent vendored complexity from dominating the hotspot rankings and allow genuine first-party risk to surface.

node_modules/yaml/dist/resolveSeq-d03cb037.jspackages/framework/src/jsonSchemaFaker.js

This function is vendored YAML parser code — specifically a sequence-resolution routine from the yaml package — that has been bundled or inlined into jsonSchemaFaker.js rather than consumed as a proper node_modules dependency. At a cyclomatic complexity of 257, a max nesting depth of 9, and a fan-out of 146, it carries every structural anti-pattern Hotspots tracks: complex branching across hundreds of independent execution paths, deeply nested control structures that make local reasoning nearly impossible, and god-function coupling into 146 distinct callees. The exit-heavy and long-function patterns compound this — there are many return paths through an already vast surface area, each one a potential test gap.

The activity signal is what makes this urgent rather than merely ugly. It was touched 1 time in the last 30 days and shows 0 days since last change, landing it squarely in the fire quadrant. That single recent commit on a file where every recorded commit has been classified as a bug fix is a flag worth taking seriously. I would not rewrite the YAML parser. I would replace this inlined copy with a direct dependency on the yaml package, add jsonSchemaFaker.js to the .hotspotsrc.json exclude list, and re-run the analysis to see what first-party critical functions were previously obscured by this file’s dominance.

src/shared.jspackages/framework/src/jsonSchemaFaker.js

This is the more extreme of the two. A cyclomatic complexity of 468 is not a refactoring problem — it is a signal that an entire bundled library module has been concatenated into a single analyzed unit. The fan-out of 382 means this function (or function-shaped blob) calls into 382 distinct locations, making it the structural center of gravity for everything jsonSchemaFaker.js touches. Nesting depth of 9 places it firmly in the range where even experienced engineers need external tooling to reason about control flow.

Like its neighbor, this was touched 1 time in the last 30 days with 0 days since last change, and every recorded commit on the file has been classified as a bug fix. Both functions share identical external signal profiles: one total commit, one author active in the last 90 days, no reverts. That sparse history on a file of this structural magnitude suggests it isn’t being actively reviewed at the depth the complexity warrants. The immediate action is the same as above: unbundle this third-party code, exclude the file from first-party analysis, and treat the schema-faking logic itself as the unit of ownership.

processSdkMessagepackages/novu/src/commands/wizard/pipeline/steps/run-agent.ts

With CC 49, ND 6, and FO 36, processSdkMessage is the most structurally significant first-party function in this snapshot. Its location in a wizard pipeline step for running an AI agent suggests it handles message dispatch and response routing for novu’s agentic workflow feature — a path that likely branches on message type, role, streaming state, and error conditions. CC 49 means 49 independent execution paths through a single function; ND 6 puts it well into the range where nested conditionals become hard to reason about without tooling. The FO of 36 adds coupling risk: changes here propagate to 36 distinct callees. At Risk 18.9, this is the fire-quadrant function most worth addressing once the vendor noise in jsonSchemaFaker.js is excluded. I’d extract the message-type branching into a dispatch table or dedicated handler functions, targeting a post-refactor CC below 15.

objectTypepackages/framework/src/jsonSchemaFaker.js

Unlike the two vendored functions above it, objectType is application-owned schema-generation logic — the code that constructs fake object values when given a JSON Schema object type. At CC 70 and ND 5, it carries the full weight of JSON Schema’s object constraints: required properties, additionalProperties, and allOf/anyOf/oneOf combinators each add branching paths, with FO 47 reflecting the range of schema-handling routines it delegates into. If the team excludes jsonSchemaFaker.js from hotspot tracking entirely, this function goes with it — but it’s worth examining first. The combinators and property-iteration logic are natural extraction points that would bring CC into testable range and clarify ownership of each schema constraint.

detectPackageManagerTypepackages/add-inbox/src/config/package-manager.ts

detectPackageManagerType has CC 25 and ND 8 — a classic detection function that probes the filesystem and environment for lockfile presence, version manager config, and workspace conventions across npm, yarn, pnpm, bun, and likely others. ND 8 is the metric to act on here: at that nesting depth, a reader must hold eight levels of conditional state simultaneously to follow any single branch. FO is only 8, so coupling isn’t the concern. Flattening this with early-return guard clauses — one detector function per package manager, one top-level dispatcher — would halve the complexity and make adding a new package manager a self-contained change rather than a surgery inside a nested conditional tree.

Key Takeaways

  • Unbundle jsonSchemaFaker.js first. Both critical-band functions are vendored code living inside packages/framework/src/jsonSchemaFaker.js. Replacing them with proper package dependencies and excluding the file via .hotspotsrc.json will surface the true first-party picture — processSdkMessage (Risk 18.9) and objectType (Risk 18.6) become the top priorities once the vendor noise is cleared.
  • processSdkMessage is the most urgent first-party refactor. At CC 49, ND 6, and FO 36, it is both structurally complex and actively changing. Extracting its message-type branching into a dispatch table is a tractable refactor that directly reduces live regression risk in novu’s agentic workflow path.
  • detectPackageManagerType is a quick win. CC 25 with ND 8 is a straightforward flattening problem — early-return guard clauses per package manager will cut complexity in half and make future additions self-contained.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
deeply_nested5
exit_heavy5
god_function4
long_function4
hub_function1

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/novuhq/novu
cd novu
git checkout 60720ba12c8120cf04000755eb6ced705be0e96c
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 →

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