Skip to main content
Engineering

The Dependency Diligence Pass: What npm audit, Lockfile Age, and Transitive Bloat Revealed in Four Client Codebases

A dependency-layer diligence method that turns npm audit noise, lockfile age, and native-module blockers into a number an investor can actually price.

2026-09-12 · By Filip Lauc

Why dependency rot is the diligence dimension nobody scores

Dependency rot is the one diligence dimension where remediation cost can be estimated before you read a single line of business logic. Lockfile age, major versions behind, and unmaintained transitive packages are measurable in an afternoon, and unlike code quality they translate directly into engineer-days that an acquirer or investor can put in a model.

Most technical due diligence reports treat dependencies as a bullet under "security": they paste the npm audit summary, note the count of high and critical findings, and move on. That bullet is close to useless. An npm audit total of 40 can mean one deprecated build-time transitive package pulled in by a test runner, or it can mean the auth library has an unpatched token-parsing bug reachable from an unauthenticated endpoint. The count alone does not separate them.

What a buyer actually needs to know is narrower and harder: if we take this codebase over on Monday, how many weeks of engineering are consumed before anyone can ship a feature safely, and which upgrades are blocked by something other than effort. That question has a defensible answer. The rest of this post is the method we use to produce it, and the pattern it surfaced across four production codebases we either inherited or have maintained long-term.

The five measurements that make up a dependency risk sheet

A dependency risk sheet has five columns: lockfile age, majors behind on runtime-critical packages, unmaintained transitive count, blocked upgrades with named blockers, and remediation cost in engineer-days. Each is cheap to collect and each maps to a different failure mode, which is why a single composite score hides more than it reveals.

Lockfile age is the single most predictive number, and almost nobody records it. It is not the date of the last commit to package-lock.json or yarn.lock; a Dependabot bump to a dev dependency last week does not mean the tree is fresh. What matters is the median install date across direct runtime dependencies. A tree whose median entry has not moved in two years behaves very differently from one where a handful of leaf packages are stale, because the former means every upgrade path now crosses multiple breaking majors at once.

Majors behind should be counted only on packages that ship to production or gate the build. Counting every dev dependency inflates the number and demoralises whoever has to read the report. Unmaintained transitive count means packages with no release and no commit activity in over 24 months that appear in the resolved tree, weighted by whether they sit on a request path. And blocked upgrades are the column that separates a real assessment from a generated one, because a blocker has a name: a native module, a peer dependency range, a fork, a patch file, or an internal package nobody has the source for.

  • Lockfile age: median resolved-install date across direct runtime dependencies, not last-commit date of the lockfile
  • Majors behind: counted on runtime and build-gating packages only, with the framework version treated separately
  • Unmaintained transitive count: no release or commit in 24+ months, weighted by whether the package sits on a request or render path
  • Blocked upgrades: each with a named blocker (native module, peer range, fork, patch-package entry, unavailable internal package)
  • Remediation cost: engineer-days to reach a state where the next routine upgrade is a one-day job

Why npm audit output is the least useful number on the page

npm audit reports vulnerabilities in the resolved tree without knowing whether the vulnerable code path is reachable, whether the package runs at build time or request time, or whether your usage touches the affected function. It is a starting list, not a risk assessment, and treating its totals as a diligence metric produces false alarm and false comfort in roughly equal measure.

The triage that makes it useful takes about two hours and has three questions per finding. First, does this package execute in production, or only during build and test? A vulnerable dev-time package matters for supply-chain integrity of your CI, which is a real concern, but it is a different concern from a request-path issue. Second, is the vulnerable function in the call graph at all? Many advisories cover a parsing helper or a CLI entry point that an app importing one exported function never touches. Third, is there a fix available in a non-breaking version, or does the fix require a major bump of the parent that pulls it in?

That third question is where the real cost hides. A critical finding fixed by a patch release is ten minutes of work. The same finding, when the fix only exists in a major release of a framework two majors ahead of you, is the framework upgrade you have been avoiding, wearing a security label. We have seen single advisories whose honest remediation estimate was three weeks, and others marked high that were closed by one line in an overrides block. A diligence report that does not distinguish these two has not done the work.

What four production codebases looked like at the dependency layer

Across four codebases we work in, the dependency picture split cleanly by platform, not by team quality. Mobile React Native codebases carried the highest blocked-upgrade count and the highest per-upgrade cost. Long-lived web codebases on a managed backend carried the highest transitive bloat but the lowest blocker count. The pattern held regardless of who wrote the code originally.

The React Native apps are the clearest case. On mobile, upgrades are not a package.json problem, they are a native build problem. A React Native major bump touches Gradle configuration, Podfile and CocoaPods resolution, Xcode project settings, and every third-party library with a native component: push notifications, camera, biometrics, payments SDKs, analytics. Each one has its own compatibility window. The dependency you cannot move is rarely the one npm flags; it is the SDK whose vendor has not shipped a version compatible with the new architecture. That is a named blocker, it goes on the sheet, and it is the difference between a three-day upgrade and a three-week one.

The long-lived web side looks different. A codebase built on a managed backend platform and maintained for years accumulates transitive bloat through build tooling more than through runtime libraries: bundler plugins, PostCSS chains, test utilities, polyfills for browsers nobody targets any more. Runtime dependencies stay comparatively thin because the platform SDK absorbs most of the surface. The remediation there is cheaper per item but broader, and the highest-value move is usually deleting packages rather than upgrading them. In the repositories we keep public and can inspect directly, the direct dependency count is deliberately small, and that is the property that keeps upgrade cost flat over time.

The fourth pattern is a product you maintain yourself, where you control the release cadence and therefore never accumulate a cliff. Upgrade cost stays near zero not because the tooling is better but because the interval between upgrades never exceeds the window in which a migration guide is still accurate.

The three upgrades an investor would have priced in

Across these codebases, three upgrade categories account for nearly all of the remediation cost an investor should price: the framework major that unlocks everything else, the native-module chain on mobile, and the build toolchain that silently gates CI. Everything else is routine patching that fits inside normal sprint capacity.

The framework major is first because it is a prerequisite. When the mobile framework, the web framework, or the platform SDK sits two or more majors behind, most other upgrades are blocked by peer ranges that resolve only against the newer version. Pricing individual package upgrades before the framework moves produces a number that is wrong in both directions: too high because the work overlaps, too low because it assumes each upgrade can be done independently. Price the framework move first, then price what remains.

The native-module chain is second and is the only category where money cannot always substitute for time. If a vendor SDK has no compatible release, your options are: wait, fork and patch, replace the vendor, or write the native module yourself. Three of those four are multi-week and one of them is out of your control entirely. This is the item that belongs in a diligence report with a name, a vendor, and a status, not a line item labelled "mobile upgrades".

The build toolchain is third and is the one buyers most often miss because it produces no user-visible symptom until it does. Node version pinned to a release that has left security support, a bundler major behind, a CI image that can no longer install the toolchain from scratch. The tell is simple and worth running during diligence: delete node_modules, delete any local cache, and do a clean install and production build on a fresh machine. If that fails, or requires undocumented steps, the onboarding cost for every future engineer is already baked in.

  • Framework major: price first, because peer ranges make it a prerequisite for most other upgrades
  • Native-module chain: each blocked SDK named individually with vendor and status, since not all are solvable with effort alone
  • Build toolchain and runtime: validated with a clean-machine install and production build, not by reading the CI config

Turning the sheet into engineer-days a report can defend

Convert the risk sheet into engineer-days by estimating each blocked upgrade separately, adding a fixed regression-testing allowance per release-affecting change, and stating the assumptions that would invalidate the number. The output should be a range with named drivers, not a single figure, and it should define the end state it buys.

The end state matters more than the number. "Bring dependencies up to date" is not an end state, because it is never finished. The end state we write into a diligence note is: the next routine upgrade takes one engineer one day, a clean install and production build succeeds on a fresh machine, and no runtime dependency is more than one major behind. That is testable, and it tells the buyer what they are buying.

Two estimation rules keep the number honest. First, estimate the framework major on its own and treat the packages it unblocks as a second, smaller line item rather than summing them independently; that avoids double-counting the same test cycle three times. Second, add a separate allowance for anything with a native build or an app store release in the path, because a mobile upgrade is not done when it compiles, it is done when a build has passed review and rolled out without a crash-rate regression. That gap between merged and shipped is where naive estimates lose a week.

If you are the one being diligenced, the same sheet is worth producing before anyone asks. It converts a vague reviewer concern into a scoped, priced item you control the framing of, and in our experience an honest three-week remediation estimate reads far better than a green npm audit summary that a reviewer spends an hour disproving.

Key Takeaways

  • Lockfile age, measured as the median resolved-install date across direct runtime dependencies, predicts remediation cost better than any npm audit total.
  • npm audit counts are a starting list, not a risk score: triage each finding by production reachability and whether the fix requires a breaking major.
  • Blocked upgrades must be named individually (native module, peer range, fork, patch file), because named blockers are what separate a 3-day upgrade from a 3-week one.
  • Three categories carry nearly all the cost: the framework major, the native-module chain on mobile, and the build toolchain that gates CI.
  • Price remediation against a testable end state, such as the next routine upgrade taking one engineer one day and a clean install succeeding on a fresh machine.

If you want to run this method against a real tree rather than a hypothetical one, our public SvelteKit and Firebase repositories are open and their lockfiles can be inspected directly.

Frequently Asked Questions

How long should a dependency due diligence pass on a startup codebase take?

For a single repository, collecting the measurements takes a few hours: lockfile age, majors behind, unmaintained transitive packages, and a clean-machine install and build. Triaging the npm audit findings by reachability adds another two to three hours. The part that takes longer is identifying named upgrade blockers, since that requires reading the native build configuration and checking vendor SDK compatibility, typically a full day for a mobile app.

Is a high npm audit count a deal-breaker in technical due diligence?

No, on its own it tells you almost nothing. A high count is frequently driven by build-time and test-time transitive packages that never execute in production. What matters is whether any vulnerable code is reachable on a request path, and whether the fix exists in a patch release or only behind a breaking major upgrade of a parent package.

What is a reasonable lockfile age for a healthy production codebase?

As a rough working threshold, a median resolved-install date within the last six to nine months across direct runtime dependencies indicates a codebase where upgrades are routine. Past 18 to 24 months, upgrade paths start crossing multiple breaking majors simultaneously and the cost per upgrade rises sharply. Age matters more than count, because it determines whether migration guides still apply cleanly.

Should we upgrade dependencies before or after taking over an inherited codebase?

Do the measurement before takeover and the upgrade work immediately after, in a dedicated window rather than interleaved with feature work. Upgrading before you understand the test coverage means you cannot tell a regression from a pre-existing bug. Upgrading alongside features means every failure has two possible causes and debugging time roughly doubles.

How do you estimate the cost of a React Native major version upgrade?

Estimate the core framework and native build configuration separately from the third-party libraries with native components, then check each of those libraries for a compatible release. Libraries with no compatible version become named blockers with their own resolution path: wait, fork, replace, or reimplement. Add an allowance for the gap between a green build and a shipped release, since app store review and crash-rate validation sit outside engineering time.

Filip Lauc

Written by

Filip Lauc

CEO, Jaspero

Filip Lauc is the CEO of Jaspero, a software development agency based in Osijek, Croatia. A full-stack JavaScript developer with over a decade of experience across Angular, Svelte, and Node.js, he leads Jaspero's work as a long-term embedded engineering partner for clients like GlycanAge, where his team has served as the dedicated engineering team for six years.

Let's Build Together

Your vision,
our expertise.

From AI integration to full-stack development, we turn ambitious ideas into products that perform.