next-leak 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -17
- package/dist/{chunk-EJ26OBZI.js → chunk-5ZBIS5WM.js} +34 -2
- package/dist/{chunk-LFGWGM5Z.js → chunk-75W2KIXN.js} +1457 -1127
- package/dist/{chunk-WXAFXVWS.js → chunk-AUUMRTZZ.js} +5 -1
- package/dist/{chunk-RS3AO7HB.js → chunk-SV47LIER.js} +16 -3
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli.js +50 -5
- package/dist/heap-diff.d.ts +18 -1
- package/dist/{html-report-TN7PYVU4.js → html-report-YWIK5EDQ.js} +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -4
- package/dist/{issue-report-HKA26WNV.js → issue-report-FPVOZ6HU.js} +1 -1
- package/dist/launcher.d.ts +20 -1
- package/dist/load.d.ts +19 -0
- package/dist/ritual.d.ts +30 -0
- package/dist/route-guidance.d.ts +19 -0
- package/dist/runner.d.ts +58 -0
- package/dist/self-check.d.ts +50 -0
- package/dist/trend.d.ts +22 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,20 +29,21 @@ next-leak found the growth, the retaining object and the chain that holds it —
|
|
|
29
29
|
without being told what to look for. Next.js 16.3.0 has since fixed it.
|
|
30
30
|
|
|
31
31
|
**Verified against real Next.js issues**, not synthetic fixtures. Issue states
|
|
32
|
-
checked 2026-08-
|
|
32
|
+
checked 2026-08-27:
|
|
33
33
|
|
|
34
34
|
| Issue | What it is | Measured | State today |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
+
| [#97938](https://github.com/vercel/next.js/issues/97938) | `cacheComponents`: composite abort signal never released | +705 KB per request on 16.3.3, flat on 16.2.6 | **open** |
|
|
36
37
|
| [#96533](https://github.com/vercel/next.js/issues/96533) | ISR revalidation holds RSC buffers between collections | 4–5 MB of `arrayBuffers` held vs 0.32 MB retained | **open** |
|
|
37
|
-
| [#97464](https://github.com/vercel/next.js/issues/97464) | Static-gen worker retains per prerendered page |
|
|
38
|
+
| [#97464](https://github.com/vercel/next.js/issues/97464) | Static-gen worker retains per prerendered page | OOM after 1617 and 1525 of 2504 pages on 16.3.3; 16.2.12 finishes at 0.05 MB/page | **open** |
|
|
38
39
|
| [#92287](https://github.com/vercel/next.js/issues/92287) | Cache Components: unbounded `arrayBuffers` under load | 37.5 MB of arrayBuffers held between collections, 37x what it retains (16.3.1) | **open** |
|
|
39
40
|
| [#84884](https://github.com/vercel/next.js/issues/84884) | axios + `AbortSignal` in middleware | 32.8 → 369.9 MB | **open** |
|
|
40
|
-
| [#89091](https://github.com/vercel/next.js/issues/89091) | zlib retention on mid-stream aborts | +42.5 MB/1000 aborted req on 16.1.5; **+0.03 on 16.3.1** |
|
|
41
|
+
| [#89091](https://github.com/vercel/next.js/issues/89091) | zlib retention on mid-stream aborts | +42.5 MB/1000 aborted req on 16.1.5; **+0.03 on 16.3.1** | closed |
|
|
41
42
|
| [#95094](https://github.com/vercel/next.js/issues/95094) | Middleware `setTimeout` ids retained by the sandbox | 112 MB retained; flat after the fix | fixed in 16.3.0 |
|
|
42
43
|
| [#94890](https://github.com/vercel/next.js/issues/94890) | Router LRU cache doesn't count its keys | 26.7 → 71.9 MB | fixed in 16.3.0 |
|
|
43
44
|
| [#94919](https://github.com/vercel/next.js/issues/94919) | Retention on client aborts | 39 → 139 MB · [with a caveat](#scope-and-limits-read-before-filing-issues) | fixed in 16.3.0 |
|
|
44
45
|
|
|
45
|
-
The
|
|
46
|
+
The closed ones are kept deliberately: a tool that only lists open bugs
|
|
46
47
|
looks impressive until the bugs close, and what those rows show is that the
|
|
47
48
|
measurements matched what the fixes turned out to be. #94919 is the sharpest —
|
|
48
49
|
the PR that closed it discarded the RSC-WeakMap hypothesis in the title and
|
|
@@ -73,11 +74,12 @@ provide heap snapshots taken after forced GC"* — which almost nobody produces
|
|
|
73
74
|
correctly. `next-leak` runs that controlled measurement for you and answers with
|
|
74
75
|
evidence a maintainer would accept.
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
The answers, all valuable:
|
|
77
78
|
|
|
78
79
|
1. **You don't have a leak** — the spike is transient and drains during idle (the most common case).
|
|
79
|
-
2. **
|
|
80
|
-
3. **
|
|
80
|
+
2. **Something is filling up, not leaking** — a bounded cache on its way to its ceiling, which grows every cycle and still is not a leak.
|
|
81
|
+
3. **The leak is in your code (or a dependency)** — named down to the source file when possible.
|
|
82
|
+
4. **It looks like framework internals** — with a ready-to-file issue draft.
|
|
81
83
|
|
|
82
84
|
## Quickstart
|
|
83
85
|
|
|
@@ -120,7 +122,9 @@ Every report prints the gate it used.
|
|
|
120
122
|
| `--max-old-space <mb>` | 512 | Heap cap of each measured process. Raise it for apps whose legitimate working set is larger, or they die under measurement |
|
|
121
123
|
| `--quick` | off | Fast preset (2000 requests × 4 cycles, 8s idle) — the exact profile the real-app validation ran with. Same cycle count as the default; what it trades away is traffic per cycle, so it sits on the noise floor and is less sensitive to slow leaks. Explicit flags override it |
|
|
122
124
|
| `--no-resolve` | off | Skip the second pass on inconclusive routes |
|
|
125
|
+
| `--self-check` | off | Measure a planted leak first to prove the harness works here. Costs one route's worth of time; a run that cannot detect 8 KB per request produces verdicts worth nothing |
|
|
123
126
|
| `--diff-all` | off | Diff snapshots for stable routes too |
|
|
127
|
+
| `--attribute` | off | **`build` only.** Also name *what* the worker retains, not only that it retains |
|
|
124
128
|
| `--output <dir>` | `<app>/.next-leak` | Where runs are written |
|
|
125
129
|
| `--write-config` | off | Write `next-leak.config.json` for the routes that need sample params, then exit. Never overwrites an existing file |
|
|
126
130
|
|
|
@@ -135,21 +139,36 @@ A large site can run out of heap while prerendering, before any server exists to
|
|
|
135
139
|
measure ([#97464](https://github.com/vercel/next.js/issues/97464)). That command
|
|
136
140
|
runs your build unmodified and samples the resident memory of each
|
|
137
141
|
static-generation worker. It needs neither a previous build nor standalone
|
|
138
|
-
output, and takes `--output`
|
|
142
|
+
output, and takes `--output` and `--attribute`.
|
|
143
|
+
|
|
144
|
+
The report also prints what the build's **own process** reached, separately from
|
|
145
|
+
the worker's figure and labelled *reported, not judged*. The two are never added
|
|
146
|
+
together: the parent sheds memory while workers climb — 1.43 GB down to 0.10 GB
|
|
147
|
+
over the same window on the #97464 reproduction — so summing them cancels the
|
|
148
|
+
finding the worker verdict rests on. It is there because a build can fail
|
|
149
|
+
entirely in the parent, during compilation or file tracing, where no worker
|
|
150
|
+
exists yet.
|
|
139
151
|
|
|
140
152
|
Dynamic routes need sample params in `next-leak.config.json` in your app dir:
|
|
141
153
|
|
|
142
154
|
```json
|
|
143
155
|
{
|
|
144
156
|
"params": { "lang": "en" },
|
|
145
|
-
"routes": { "/products/[id]": { "id": "42" } },
|
|
157
|
+
"routes": { "/products/[id]": { "id": "42-{n}" } },
|
|
146
158
|
"headers": { "accept-encoding": "gzip, br", "cookie": "session=..." }
|
|
147
159
|
}
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
`--write-config` generates that file for you
|
|
151
|
-
build already prerendered
|
|
152
|
-
|
|
162
|
+
`--write-config` generates that file for you. It takes the *shape* of each value
|
|
163
|
+
from paths your build already prerendered, and makes the value itself move:
|
|
164
|
+
`post-0` becomes `post-{n}`, and a value with no trailing number gets one —
|
|
165
|
+
`seed` becomes `seed-{n}`. A value the build prerendered is the one value
|
|
166
|
+
guaranteed not to measure anything — every request hits the same warm cache
|
|
167
|
+
entry, so the route reads as flat whatever it retains, and that false negative
|
|
168
|
+
lands on exactly the leaks being reported now (`use cache`, `cacheComponents`
|
|
169
|
+
and ISR all key on the params). If your app answers 404 for params it never
|
|
170
|
+
prerendered, the run says so through its non-2xx count; drop the marker then.
|
|
171
|
+
When a run skips a route it prints the same fragment.
|
|
153
172
|
|
|
154
173
|
```json
|
|
155
174
|
```
|
|
@@ -196,6 +215,7 @@ separates them, because each one has a different fix:
|
|
|
196
215
|
| One-time warm-up growth (JIT, lazy caches) | `stable` | The first cycle is excluded from the verdict; warm-up flattens, leaks keep climbing |
|
|
197
216
|
| A route that is expensive, not leaky | `failed` under load it cannot sustain, flat once concurrency fits | Real leaks survive forced GC at any concurrency; saturation disappears when load drops |
|
|
198
217
|
| Growth that pauses and resumes (stepwise) | `leak` | A healthy route gives back 20-30% of its growth; a stepwise leak gives back nothing |
|
|
218
|
+
| A cache filling up under the load that measures it | `saturating` | A bounded store grows by less each cycle as new keys get rarer; a leak does not decelerate |
|
|
199
219
|
| Native/buffer memory with a flat JS heap | `leak (external)` or an explicit RSS note | Heap, `external` and RSS are sampled and judged separately |
|
|
200
220
|
| A leak in your code vs a dependency vs Next itself | `culprit: src/app/x/page.tsx (your code)` — or the package, or framework internals | Retainer chains mapped through the build's source maps |
|
|
201
221
|
| A run whose own evidence is weak | `low confidence` warnings, or the verdict is withdrawn | Every run audits itself: did the load land, did the heap settle, does one cycle carry the average, did the heap run into its own ceiling |
|
|
@@ -204,7 +224,12 @@ separates them, because each one has a different fix:
|
|
|
204
224
|
|
|
205
225
|
- **`stable`** — no growth this run could detect: across the cycles it ran, the
|
|
206
226
|
post-GC curve never cleared the growth gate printed at the foot of the
|
|
207
|
-
report.
|
|
227
|
+
report. Before trusting a page of these, prove the instrument works where you
|
|
228
|
+
are running it: `--self-check` plants a leak of 8 KB per request and measures
|
|
229
|
+
it, under your Node build, your heap cap, your concurrency and your container
|
|
230
|
+
limits. On this machine it comes back at 8.16 MB/1000 requests against a
|
|
231
|
+
theoretical 8.0 — a harness that cannot see that is one whose flat curves mean
|
|
232
|
+
nothing, and the run says so when nothing vouched for it. That is not proof of absence, and the wording matters — the verdict
|
|
208
233
|
is deliberately biased toward missing a leak rather than inventing one (a
|
|
209
234
|
single flat or falling cycle is enough to call a route stable), so a leak
|
|
210
235
|
that oscillates while it climbs can land here. To press harder, raise
|
|
@@ -214,8 +239,23 @@ separates them, because each one has a different fix:
|
|
|
214
239
|
flat but RSS keeps climbing, the report says so explicitly: that is an
|
|
215
240
|
allocator, external-buffer or fragmentation problem, not a JS-heap leak.
|
|
216
241
|
- **`leak`** — the report names the culprit when attribution resolves: your file (`culprit: src/app/x/page.tsx (your code)`), a dependency (package name), or framework internals. An `ISSUE-<route>.md` draft is generated **when the evidence plainly supports the verdict** — a `leak` carrying low-confidence warnings (growth barely over the threshold, one cycle dominating the mean, too few cycles for its size) gets the verdict but no draft, because a draft is written to be pasted into someone else's tracker. If the leak is app-owned, the draft tells you **not** to file it upstream.
|
|
242
|
+
- **`saturating`** — every cycle grew, but by less than the one before, and the
|
|
243
|
+
last by less than half the first. That is the shape of a bounded store
|
|
244
|
+
running out of new keys, not of memory going missing. It matters because the
|
|
245
|
+
alternative was calling it a leak: a `use cache` route measured on Next
|
|
246
|
+
16.3.3 came out at +603 MB per 1000 requests that was entirely the cache
|
|
247
|
+
storing what it had been asked to store — the same route dropped to +88 MB
|
|
248
|
+
once the payload was removed. Because the shape requires every cycle to clear
|
|
249
|
+
the growth gate, a decelerating curve always ends the window still growing,
|
|
250
|
+
so where it settles is outside what was measured: these routes are
|
|
251
|
+
**measured again** with twice the cycles, like `inconclusive` ones. No issue
|
|
252
|
+
draft is generated. When the load was driving a cache with keys it had never
|
|
253
|
+
served, the report says so on any growing route and points at `{n%N}` to
|
|
254
|
+
bound the key set — measure again that way before believing the number.
|
|
217
255
|
- **`inconclusive`** — the evidence does not decide. The run does not stop there: any inconclusive route is **measured again automatically**, with twice the cycles, and the second pass is what you see (`resolved at 8 cycles` next to the verdict). On the reproduction for [#95094](https://github.com/vercel/next.js/issues/95094), `--quick` alone reports `inconclusive` on three deltas and then comes back with the leak. `--no-resolve` turns the second pass off; when even that is undecided, the re-run command is still printed.
|
|
218
|
-
- **`failed`** — the route errored under load (auth redirects, POST-only endpoints). >1% non-2xx aborts measurement instead of measuring garbage. That's by design.
|
|
256
|
+
- **`failed`** — the route errored under load (auth redirects, POST-only endpoints). >1% non-2xx aborts measurement instead of measuring garbage. That's by design. A process that died of **heap exhaustion** is not one of these: it reports `leak`, because a route that could not survive its own load did not fail to be measured — it was measured right up to the point where it stopped fitting. The verdict comes from that outcome, not from the shape of the truncated curve, which is the same rule `next-leak build` applies to a static-generation worker that dies. The run prints the cycles it survived and the growth up to the death, and exits 0 with a finding rather than 1 with an error.
|
|
257
|
+
|
|
258
|
+
When a route is configured with a varying sample value (`{n}`) and *every* request comes back non-2xx, the run checks whether the value is the problem before blaming the route: it requests one value the marker would produce and one the build prerendered, and only when those disagree does it say so. An app with `generateStaticParams` and `dynamicParams = false` answers 404 for anything outside its param set, which is indistinguishable from a broken route in the counters alone. Bound the marker to the params that exist (`{n%N}`) or drop it.
|
|
219
259
|
|
|
220
260
|
## Peak pressure: `stable` is not the same as safe
|
|
221
261
|
|
|
@@ -330,6 +370,20 @@ than a false accusation, and the warnings are on the report either way.
|
|
|
330
370
|
└── <nn>-<route>/ # raw baseline/after .heapsnapshot per route
|
|
331
371
|
```
|
|
332
372
|
|
|
373
|
+
`report.html` is the one you send to someone else. It is a real run below —
|
|
374
|
+
three routes measured with `--self-check`, on the reproduction this repo tests
|
|
375
|
+
against:
|
|
376
|
+
|
|
377
|
+
<img src="https://raw.githubusercontent.com/xabierlameiro/next-leak/main/docs/report-example.png" alt="next-leak HTML report: two stable routes and one leaking at +471 MB per 1000 requests, attributed to lib/sink.ts" width="720">
|
|
378
|
+
|
|
379
|
+
Per route it draws the post-GC curve, the peak reached *during* each cycle
|
|
380
|
+
across heap, external, arrayBuffers and RSS, and what grew between the two
|
|
381
|
+
snapshots — with its retained size and, when the source maps resolve it, the
|
|
382
|
+
file that owns it. No JavaScript, no external requests, no
|
|
383
|
+
fonts to fetch: it opens offline from a CI artifact. The file above is
|
|
384
|
+
[`docs/report-example.html`](./docs/report-example.html) if you want to poke at
|
|
385
|
+
the markup.
|
|
386
|
+
|
|
333
387
|
Snapshots are the ground truth: load them in Chrome DevTools (Memory → Load → Comparison) and check every claim yourself. Runs accumulate — each keeps its snapshots (tens of MB per route); delete old timestamp folders when done.
|
|
334
388
|
|
|
335
389
|
## Why not just use…
|
|
@@ -350,6 +404,7 @@ through the build's source maps.
|
|
|
350
404
|
|
|
351
405
|
## Scope and limits (read before filing issues)
|
|
352
406
|
|
|
407
|
+
|
|
353
408
|
- **Supported (default command):** App Router · `output: "standalone"` · Node ≥ 22 · Linux/macOS. Pages Router, non-standalone, and Windows are rejected with a clear message.
|
|
354
409
|
- **Sample values can vary per request.** `"slug": "post-{n}"` gives every
|
|
355
410
|
request its own URL — the shape of bot traffic and of a cache that never
|
|
@@ -375,10 +430,16 @@ through the build's source maps.
|
|
|
375
430
|
once for an hour and a half. Use it on a reproduction you are investigating,
|
|
376
431
|
not on a build you need to finish. It also covers only a low slice of the
|
|
377
432
|
curve, and says which: a snapshot weighs roughly 0.4x to 0.8x the worker's
|
|
378
|
-
resident size
|
|
379
|
-
|
|
380
|
-
report states what share of the observed growth they span (19% on that
|
|
433
|
+
resident size — both captures happen well below the ceiling, and the report
|
|
434
|
+
states what share of the observed growth they span (19% on that
|
|
381
435
|
reproduction, whose worker peaks near 4 GB).
|
|
436
|
+
|
|
437
|
+
That ceiling is not the file's size. memlab reads `nodes`, `edges` and
|
|
438
|
+
`locations` as typed arrays in chunks and only parses the rest, so what has
|
|
439
|
+
to fit in a 512 MB V8 string is everything else — and on a leaking snapshot
|
|
440
|
+
that is almost nothing. A 1342.9 MB capture of a leaking route parsed 2.4 MB
|
|
441
|
+
of JSON and diffs fine; a 2227.5 MB one whose `strings` section alone was
|
|
442
|
+
868.9 MB is refused, and the message names 869 MB rather than 2227 MB.
|
|
382
443
|
- **Architectures:** verified on **arm64 and x64** (linux/amd64 in Docker) — same app, same parameters, same verdicts.
|
|
383
444
|
- **Attribution** (naming the file) needs a Turbopack build with server sourcemaps — the Next 15+ default. On webpack builds the registry is empty by design and findings degrade to `unattributed` with raw retainer chains; measurement itself does not depend on it. Note that `output: "standalone"` + `--webpack` produced a bundle that could not start at all on `16.3.0-canary.90` (missing `@swc/helpers`), independently of this tool.
|
|
384
445
|
- Empirically validated on Next **15.5.4, 16.0.x, 16.1.5, 16.2.x, 16.3.0/16.3.1 and 16.3-canary** (incl. Sentry, OpenTelemetry, PPR and i18n apps), against the public reproductions attached to real issues (open and since-fixed). Most recent measurements, 2026-08-17/18: the runtime path on 16.2.12 and 16.3.1-canary.18, the build path on 16.2.12 and 16.3.1. The contracts it relies on are stable since Next 13–14, but older versions are untested.
|
|
@@ -9,6 +9,8 @@ function minGrowthFor(requestsPerCycle) {
|
|
|
9
9
|
var STEPWISE_MIN_GROWING_CYCLES = 2;
|
|
10
10
|
var STEPWISE_MAX_DRAWDOWN_RATIO = 0.1;
|
|
11
11
|
var ACQUITTAL_MAX_GROWTH_MULTIPLE = 8;
|
|
12
|
+
var SATURATION_MIN_CYCLES = 3;
|
|
13
|
+
var SATURATION_MAX_FINAL_RATIO = 0.5;
|
|
12
14
|
function maxDrawdown(samples) {
|
|
13
15
|
let peak = samples[1] ?? 0;
|
|
14
16
|
let worst = 0;
|
|
@@ -29,11 +31,29 @@ function isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth) {
|
|
|
29
31
|
}
|
|
30
32
|
return maxDrawdown(samples) <= netGrowth * STEPWISE_MAX_DRAWDOWN_RATIO;
|
|
31
33
|
}
|
|
34
|
+
function isSaturating(deltas) {
|
|
35
|
+
if (deltas.length < SATURATION_MIN_CYCLES) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const first = deltas[0];
|
|
39
|
+
const last = deltas[deltas.length - 1];
|
|
40
|
+
if (first === void 0 || last === void 0 || first <= 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (let i = 1; i < deltas.length; i += 1) {
|
|
44
|
+
const current = deltas[i];
|
|
45
|
+
const previous = deltas[i - 1];
|
|
46
|
+
if (current === void 0 || previous === void 0 || current >= previous) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return last <= first * SATURATION_MAX_FINAL_RATIO;
|
|
51
|
+
}
|
|
32
52
|
function isTooLargeToAcquit(deltas, mean, minGrowth) {
|
|
33
53
|
const netGrowth = deltas.reduce((sum, delta) => sum + delta, 0);
|
|
34
54
|
return mean >= minGrowth * ACQUITTAL_MAX_GROWTH_MULTIPLE && netGrowth > 0;
|
|
35
55
|
}
|
|
36
|
-
function
|
|
56
|
+
function classifyShape(samples, options) {
|
|
37
57
|
const minGrowth = options.minGrowthPerCycle ?? MIN_GROWTH_NOISE_FLOOR;
|
|
38
58
|
if (samples.length < 4) {
|
|
39
59
|
return { verdict: "inconclusive", growthPerCycle: 0, deltas: [] };
|
|
@@ -52,6 +72,9 @@ function classifyTrend(samples, options = {}) {
|
|
|
52
72
|
const anyFlatOrDown = deltas.some((d) => d <= 0);
|
|
53
73
|
const growingCycles = deltas.filter((d) => d >= minGrowth).length;
|
|
54
74
|
if (allGrow) {
|
|
75
|
+
if (isSaturating(deltas)) {
|
|
76
|
+
return { verdict: "saturating", growthPerCycle: mean, deltas, source: "heap" };
|
|
77
|
+
}
|
|
55
78
|
return { verdict: "leak", growthPerCycle: mean, deltas, source: "heap" };
|
|
56
79
|
}
|
|
57
80
|
if (isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth)) {
|
|
@@ -65,10 +88,19 @@ function classifyTrend(samples, options = {}) {
|
|
|
65
88
|
}
|
|
66
89
|
return { verdict: "inconclusive", growthPerCycle: mean, deltas, source: "heap" };
|
|
67
90
|
}
|
|
91
|
+
function classifyTrend(samples, options = {}) {
|
|
92
|
+
const result = classifyShape(samples, options);
|
|
93
|
+
return options.cacheDriven === true ? { ...result, cacheDriven: true } : result;
|
|
94
|
+
}
|
|
68
95
|
function classifyMemoryTrend(heapSamples, externalSamples, options = {}) {
|
|
69
96
|
const heap = classifyTrend(heapSamples, options);
|
|
70
97
|
const external = classifyTrend(externalSamples, options);
|
|
71
|
-
const severity = {
|
|
98
|
+
const severity = {
|
|
99
|
+
leak: 0,
|
|
100
|
+
inconclusive: 1,
|
|
101
|
+
saturating: 2,
|
|
102
|
+
stable: 3
|
|
103
|
+
};
|
|
72
104
|
if (severity[external.verdict] < severity[heap.verdict]) {
|
|
73
105
|
return { ...external, source: "external" };
|
|
74
106
|
}
|