next-leak 0.1.0 → 0.1.2
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 +22 -0
- package/dist/attribution.d.ts +0 -2
- package/dist/bootstrap.js +1 -1
- package/dist/chunk-2BQZCZ4Z.js +129 -0
- package/dist/{chunk-5K4WEMIS.js → chunk-5ZYZW2BL.js} +16 -2
- package/dist/{chunk-4BMPC45G.js → chunk-C3KH5Z2W.js} +11 -3
- package/dist/{chunk-OSZ6ND6E.js → chunk-PF7KYD5N.js} +12 -1
- package/dist/{chunk-UZTZWHKH.js → chunk-WHC6S57X.js} +27020 -804
- package/dist/cli.js +35 -23
- package/dist/control-client.d.ts +0 -3
- package/dist/control-server.d.ts +0 -1
- package/dist/{html-report-3JIRVRBF.js → html-report-I4PNSLKY.js} +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -5
- package/dist/{issue-report-AB62FRNM.js → issue-report-VQEUXP2E.js} +2 -2
- package/dist/issue-report.d.ts +1 -4
- package/dist/module-registry.d.ts +0 -14
- package/dist/route-config.d.ts +2 -1
- package/dist/run-report.fixture.d.ts +0 -1
- package/dist/runner.d.ts +0 -12
- package/dist/vlq.d.ts +0 -1
- package/package.json +3 -6
- package/dist/chunk-K5PZFVJH.js +0 -103
package/dist/cli.js
CHANGED
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
killActiveChildren,
|
|
10
10
|
parseCliArgs,
|
|
11
11
|
runMeasurement
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-WHC6S57X.js";
|
|
13
|
+
import "./chunk-2BQZCZ4Z.js";
|
|
14
|
+
import "./chunk-PF7KYD5N.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
import { spawn } from "child_process";
|
|
@@ -36,45 +36,55 @@ function reexecWithHeadroom() {
|
|
|
36
36
|
process.exit(code ?? (signal === "SIGINT" ? 130 : 1));
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
39
|
+
function installInterruptHandlers() {
|
|
40
|
+
const aborter = new AbortController();
|
|
41
|
+
let interrupts = 0;
|
|
42
|
+
for (const signalName of ["SIGINT", "SIGTERM"]) {
|
|
43
|
+
process.on(signalName, () => {
|
|
44
|
+
interrupts += 1;
|
|
45
|
+
if (interrupts > 1) {
|
|
46
|
+
process.exit(130);
|
|
47
|
+
}
|
|
48
|
+
console.error("\n\xB7 interrupted \u2014 stopping the measured process and writing a partial run.json");
|
|
49
|
+
aborter.abort();
|
|
50
|
+
killActiveChildren();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return aborter;
|
|
54
|
+
}
|
|
55
|
+
function handleNonRunCommand(parsed) {
|
|
41
56
|
if (parsed.kind === "version") {
|
|
42
57
|
console.log(version);
|
|
43
|
-
return;
|
|
58
|
+
return true;
|
|
44
59
|
}
|
|
45
60
|
if (parsed.kind === "help") {
|
|
46
61
|
console.log(helpText(version));
|
|
47
62
|
process.exitCode = process.argv.length > 2 ? 0 : 1;
|
|
48
|
-
return;
|
|
63
|
+
return true;
|
|
49
64
|
}
|
|
50
65
|
if (parsed.kind === "error") {
|
|
51
66
|
console.error(`error: ${parsed.message}`);
|
|
52
67
|
process.exitCode = 1;
|
|
53
|
-
return;
|
|
68
|
+
return true;
|
|
54
69
|
}
|
|
55
70
|
const guardFailure = checkRuntime();
|
|
56
71
|
if (guardFailure !== null) {
|
|
57
72
|
console.error(`error: ${guardFailure}`);
|
|
58
73
|
process.exitCode = 1;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
async function main() {
|
|
79
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
80
|
+
if (handleNonRunCommand(parsed) || parsed.kind !== "run") {
|
|
59
81
|
return;
|
|
60
82
|
}
|
|
61
83
|
if (!hasHeapHeadroom()) {
|
|
62
84
|
reexecWithHeadroom();
|
|
63
85
|
return;
|
|
64
86
|
}
|
|
65
|
-
const aborter =
|
|
66
|
-
let interrupts = 0;
|
|
67
|
-
for (const signalName of ["SIGINT", "SIGTERM"]) {
|
|
68
|
-
process.on(signalName, () => {
|
|
69
|
-
interrupts += 1;
|
|
70
|
-
if (interrupts > 1) {
|
|
71
|
-
process.exit(130);
|
|
72
|
-
}
|
|
73
|
-
console.error("\n\xB7 interrupted \u2014 stopping the measured process and writing a partial run.json");
|
|
74
|
-
aborter.abort();
|
|
75
|
-
killActiveChildren();
|
|
76
|
-
});
|
|
77
|
-
}
|
|
87
|
+
const aborter = installInterruptHandlers();
|
|
78
88
|
const { options } = parsed;
|
|
79
89
|
const quickPreset = options.quick ? { loadRequests: 2e3, cycles: 4, idleMs: 8e3 } : {};
|
|
80
90
|
const report = await runMeasurement({
|
|
@@ -96,11 +106,13 @@ async function main() {
|
|
|
96
106
|
process.exitCode = 130;
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
|
-
|
|
109
|
+
try {
|
|
110
|
+
await main();
|
|
111
|
+
} catch (cause) {
|
|
100
112
|
if (cause instanceof TargetError || cause instanceof RouteConfigError) {
|
|
101
113
|
console.error(`error: ${cause.message}`);
|
|
102
114
|
} else {
|
|
103
115
|
console.error(cause);
|
|
104
116
|
}
|
|
105
117
|
process.exitCode = 1;
|
|
106
|
-
}
|
|
118
|
+
}
|
package/dist/control-client.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import type { HeapSample } from "./control-server.js";
|
|
2
|
-
export declare class ControlError extends Error {
|
|
3
|
-
constructor(message: string);
|
|
4
|
-
}
|
|
5
2
|
/** Forces GC in the measured process and returns a settled memory sample. */
|
|
6
3
|
export declare function requestGc(port: number): Promise<HeapSample>;
|
|
7
4
|
/** Forces GC, writes a named heap snapshot, and returns its path and sample. */
|
package/dist/control-server.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ export type HeapSample = {
|
|
|
12
12
|
external: number;
|
|
13
13
|
arrayBuffers: number;
|
|
14
14
|
};
|
|
15
|
-
export declare function sampleMemory(gcExposed: boolean): HeapSample;
|
|
16
15
|
export type ControlServerOptions = {
|
|
17
16
|
/** Directory where heap snapshots are written. */
|
|
18
17
|
snapshotDir: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
import {
|
|
3
3
|
renderHtmlReport
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-C3KH5Z2W.js";
|
|
5
|
+
import "./chunk-2BQZCZ4Z.js";
|
|
6
|
+
import "./chunk-PF7KYD5N.js";
|
|
7
7
|
export {
|
|
8
8
|
renderHtmlReport
|
|
9
9
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { captureEnvironment } from "./environment.js";
|
|
|
26
26
|
export type { MeasurementEnvironment } from "./environment.js";
|
|
27
27
|
export { renderHtmlReport } from "./html-report.js";
|
|
28
28
|
export { renderIssueMarkdown } from "./issue-report.js";
|
|
29
|
-
export type { MeasuredRoute } from "./
|
|
29
|
+
export type { MeasuredRoute } from "./runner.js";
|
|
30
30
|
export { routeSlug } from "./runner.js";
|
|
31
31
|
export type { RunParameters } from "./runner.js";
|
|
32
32
|
export { loadRouteConfig, resolveRoutePath, RouteConfigError, ROUTE_CONFIG_FILE } from "./route-config.js";
|
package/dist/index.js
CHANGED
|
@@ -40,15 +40,15 @@ import {
|
|
|
40
40
|
sourceIndexAt,
|
|
41
41
|
summarizeBaseline,
|
|
42
42
|
validateTarget
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-WHC6S57X.js";
|
|
44
44
|
import {
|
|
45
45
|
renderHtmlReport
|
|
46
|
-
} from "./chunk-
|
|
47
|
-
import "./chunk-
|
|
46
|
+
} from "./chunk-C3KH5Z2W.js";
|
|
47
|
+
import "./chunk-2BQZCZ4Z.js";
|
|
48
48
|
import {
|
|
49
49
|
renderIssueMarkdown
|
|
50
|
-
} from "./chunk-
|
|
51
|
-
import "./chunk-
|
|
50
|
+
} from "./chunk-5ZYZW2BL.js";
|
|
51
|
+
import "./chunk-PF7KYD5N.js";
|
|
52
52
|
export {
|
|
53
53
|
LaunchError,
|
|
54
54
|
LoadError,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
import {
|
|
3
3
|
renderIssueMarkdown
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-5ZYZW2BL.js";
|
|
5
|
+
import "./chunk-PF7KYD5N.js";
|
|
6
6
|
export {
|
|
7
7
|
renderIssueMarkdown
|
|
8
8
|
};
|
package/dist/issue-report.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export type MeasuredRoute = Extract<RouteReport, {
|
|
3
|
-
status: "measured";
|
|
4
|
-
}>;
|
|
1
|
+
import type { MeasuredRoute, RunReport } from "./runner.js";
|
|
5
2
|
/**
|
|
6
3
|
* Issue draft in the Next.js bug-template section order. The attribution
|
|
7
4
|
* gates the preamble: app/dependency-owned leaks warn against filing
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import { type LineMapping } from "./vlq.js";
|
|
2
1
|
/** Bundler module id → source path (e.g. `[project]/src/app/leaky/page.tsx`). */
|
|
3
2
|
export type ModuleRegistry = ReadonlyMap<number, string>;
|
|
4
|
-
export type NormalizedSourceMap = {
|
|
5
|
-
sources: string[];
|
|
6
|
-
lines: LineMapping[];
|
|
7
|
-
};
|
|
8
|
-
/** Accepts flat or sectioned sourcemaps; null when it is neither. */
|
|
9
|
-
export declare function normalizeSourceMap(raw: unknown): NormalizedSourceMap | null;
|
|
10
|
-
/**
|
|
11
|
-
* Extracts `[id, factory, ...]` pairs from a Turbopack CJS chunk by walking
|
|
12
|
-
* the sourcemap instead of lexing JavaScript: each factory is a contiguous
|
|
13
|
-
* region mapped to one source, so a change of source index marks a factory
|
|
14
|
-
* boundary, and the module id is the last integer literal in the unmapped
|
|
15
|
-
* gap right before it. Non-Turbopack chunks contribute nothing.
|
|
16
|
-
*/
|
|
17
3
|
export declare function extractChunkModules(code: string, rawMap: unknown): Map<number, string>;
|
|
18
4
|
/**
|
|
19
5
|
* Builds the module registry by scanning every server chunk that has an
|
package/dist/route-config.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
* route params. `params` applies globally; `routes` overrides per route
|
|
5
5
|
* template (keys as discovered, e.g. `/[lang]/candidate/[candidateId]`).
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
declare const routeConfigSchema: z.ZodObject<{
|
|
8
8
|
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9
9
|
routes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
10
10
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
@@ -34,3 +34,4 @@ export declare const UNIQUE_MARKER = "{n}";
|
|
|
34
34
|
* requests that never completed — both verified against a real server.
|
|
35
35
|
*/
|
|
36
36
|
export declare function resolveRoutePath(routeTemplate: string, config: RouteConfig): string | null;
|
|
37
|
+
export {};
|
package/dist/runner.d.ts
CHANGED
|
@@ -106,18 +106,6 @@ export type RunnerDeps = {
|
|
|
106
106
|
nextVersion: typeof readNextVersion;
|
|
107
107
|
};
|
|
108
108
|
export declare function freePort(): Promise<number>;
|
|
109
|
-
/**
|
|
110
|
-
* Filesystem-safe label for a route. Distinct routes MUST get distinct slugs:
|
|
111
|
-
* `/a/b` and `/a_b` used to collapse onto the same `ISSUE-a_b.md`, and any
|
|
112
|
-
* all-non-ASCII path (`/ñ`) became "root", colliding with `/`. A short digest
|
|
113
|
-
* disambiguates whenever the sanitized form loses information.
|
|
114
|
-
*/
|
|
115
|
-
/**
|
|
116
|
-
* Mean RSS growth per cycle, excluding the warm-up cycle exactly like the
|
|
117
|
-
* heap verdict does. Reported alongside the heap so a flat heap with growing
|
|
118
|
-
* RSS is visible instead of invisible.
|
|
119
|
-
*/
|
|
120
|
-
export declare function rssTrend(memorySamples: readonly HeapSample[]): number;
|
|
121
109
|
export declare function routeSlug(route: string): string;
|
|
122
110
|
/**
|
|
123
111
|
* Full measurement run: validate the target, discover routes, run the ritual
|
package/dist/vlq.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-leak",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
"prepublishOnly": "npm run typecheck && npm run test && npm run build",
|
|
34
34
|
"pack:smoke": "node scripts/pack-smoke.mjs",
|
|
35
35
|
"test:mutation": "stryker run",
|
|
36
|
-
"prepare": "husky >/dev/null 2>&1 || true"
|
|
37
|
-
"release": "release-it"
|
|
36
|
+
"prepare": "husky >/dev/null 2>&1 || true"
|
|
38
37
|
},
|
|
39
38
|
"pnpm": {
|
|
40
39
|
"overrides": {
|
|
@@ -46,21 +45,19 @@
|
|
|
46
45
|
"@commitlint/config-conventional": "^21.2.0",
|
|
47
46
|
"@memlab/core": "^2.0.4",
|
|
48
47
|
"@memlab/heap-analysis": "^2.0.4",
|
|
49
|
-
"@release-it/conventional-changelog": "^11.0.1",
|
|
50
48
|
"@stryker-mutator/core": "^9.6.1",
|
|
51
49
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
52
50
|
"@types/autocannon": "^7.12.7",
|
|
53
51
|
"@types/node": "^26.1.1",
|
|
54
52
|
"@types/semver": "^7.7.1",
|
|
55
53
|
"@vitest/coverage-v8": "^4.1.10",
|
|
54
|
+
"autocannon": "^8.0.0",
|
|
56
55
|
"husky": "^9.1.7",
|
|
57
|
-
"release-it": "^20.2.1",
|
|
58
56
|
"tsup": "^8.5.1",
|
|
59
57
|
"typescript": "^7.0.2",
|
|
60
58
|
"vitest": "^4.1.10"
|
|
61
59
|
},
|
|
62
60
|
"dependencies": {
|
|
63
|
-
"autocannon": "^8.0.0",
|
|
64
61
|
"semver": "^7.8.5",
|
|
65
62
|
"zod": "^4.4.3"
|
|
66
63
|
},
|
package/dist/chunk-K5PZFVJH.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
|
-
|
|
3
|
-
// src/confidence.ts
|
|
4
|
-
function effectiveVerdict(report) {
|
|
5
|
-
return report.confidence.supersededVerdict ?? report.trend.verdict;
|
|
6
|
-
}
|
|
7
|
-
var VERDICT_WEAKENING = /* @__PURE__ */ new Set([
|
|
8
|
-
"near-threshold",
|
|
9
|
-
"spiky-growth"
|
|
10
|
-
]);
|
|
11
|
-
function warrantsIssueDraft(report) {
|
|
12
|
-
return effectiveVerdict(report) === "leak" && !report.confidence.warnings.some((warning) => VERDICT_WEAKENING.has(warning.code));
|
|
13
|
-
}
|
|
14
|
-
var DEFAULT_MIN_GROWTH = 256 * 1024;
|
|
15
|
-
var LOAD_COMPLETION_FLOOR = 0.99;
|
|
16
|
-
var ABANDON_EFFECTIVE_FLOOR = 0.9;
|
|
17
|
-
var MID_STREAM_FLOOR = 0.1;
|
|
18
|
-
var SPIKE_RATIO = 4;
|
|
19
|
-
var NOISE_FLOOR_MULTIPLE = 2;
|
|
20
|
-
var mb = (bytes) => `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
21
|
-
var pct = (part, whole) => `${(part / whole * 100).toFixed(1)}%`;
|
|
22
|
-
function assessConfidence(input) {
|
|
23
|
-
const warnings = [];
|
|
24
|
-
const minGrowth = input.minGrowthPerCycle ?? DEFAULT_MIN_GROWTH;
|
|
25
|
-
const isLeak = input.trend.verdict === "leak";
|
|
26
|
-
const moving = input.settleOutcomes.filter((outcome) => outcome.status === "moving");
|
|
27
|
-
if (moving.length > 0) {
|
|
28
|
-
const phases = moving.map((outcome) => outcome.phase).join(", ");
|
|
29
|
-
warnings.push({
|
|
30
|
-
code: "unsettled",
|
|
31
|
-
detail: `the heap never held steady before sampling on ${phases} \u2014 raise --idle-ms so post-load transients finish draining`
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
const unverified = input.settleOutcomes.filter((outcome) => outcome.status === "unknown");
|
|
35
|
-
if (unverified.length > 0) {
|
|
36
|
-
const phases = unverified.map((outcome) => outcome.phase).join(", ");
|
|
37
|
-
warnings.push({
|
|
38
|
-
code: "settle-unverified",
|
|
39
|
-
detail: `the idle budget was too short to check whether the heap had settled on ${phases} \u2014 the samples may include post-load transients`
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
for (const outcome of input.loadOutcomes) {
|
|
43
|
-
if (input.abandonAfterMs !== void 0) {
|
|
44
|
-
const abandoned = outcome.abandoned ?? 0;
|
|
45
|
-
if (outcome.sent > 0 && abandoned < outcome.sent * ABANDON_EFFECTIVE_FLOOR) {
|
|
46
|
-
warnings.push({
|
|
47
|
-
code: "abandon-ineffective",
|
|
48
|
-
detail: `${outcome.phase} disconnected early on only ${abandoned} of ${outcome.sent} requests (${pct(abandoned, outcome.sent)}) \u2014 the early-disconnect path was largely not exercised`
|
|
49
|
-
});
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const midStream = outcome.abandonedMidStream ?? 0;
|
|
53
|
-
if (abandoned > 0 && midStream < abandoned * MID_STREAM_FLOOR) {
|
|
54
|
-
warnings.push({
|
|
55
|
-
code: "abandon-before-response",
|
|
56
|
-
detail: `${outcome.phase} cut ${abandoned} requests before the server sent anything (${midStream} mid-stream) \u2014 this tested pre-response disconnects, not mid-stream teardown; raise abandonAfterMs above the route's time-to-first-byte`
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const landed = outcome.ok2xx ?? 0;
|
|
62
|
-
if (outcome.sent > 0 && landed < outcome.sent * LOAD_COMPLETION_FLOOR) {
|
|
63
|
-
warnings.push({
|
|
64
|
-
code: "load-incomplete",
|
|
65
|
-
detail: `${outcome.phase} landed ${landed} of ${outcome.sent} requests (${pct(landed, outcome.sent)}) \u2014 the route saw less traffic than reported`
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const deltas = input.trend.deltas;
|
|
70
|
-
if ((isLeak || input.trend.verdict === "inconclusive") && deltas.length >= 2) {
|
|
71
|
-
const positive = deltas.filter((delta) => delta > 0);
|
|
72
|
-
if (positive.length === deltas.length) {
|
|
73
|
-
const smallest = Math.min(...positive);
|
|
74
|
-
const largest = Math.max(...positive);
|
|
75
|
-
if (largest > smallest * SPIKE_RATIO) {
|
|
76
|
-
warnings.push({
|
|
77
|
-
code: "spiky-growth",
|
|
78
|
-
detail: `one cycle grew ${mb(largest)} and another ${mb(smallest)} \u2014 the mean of ${mb(input.trend.growthPerCycle)}/cycle summarizes an uneven series; measure more cycles before quoting it`
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (isLeak && input.trend.growthPerCycle < minGrowth * NOISE_FLOOR_MULTIPLE) {
|
|
84
|
-
warnings.push({
|
|
85
|
-
code: "near-threshold",
|
|
86
|
-
detail: `growth of ${mb(input.trend.growthPerCycle)}/cycle barely clears the ${mb(minGrowth)} threshold \u2014 raise --load-requests so the signal outgrows the noise`
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
const neverSettled = input.settleOutcomes.length > 0 && moving.length === input.settleOutcomes.length;
|
|
90
|
-
const abandonedNothing = input.abandonAfterMs !== void 0 && input.loadOutcomes.length > 0 && input.loadOutcomes.every((outcome) => (outcome.abandoned ?? 0) === 0);
|
|
91
|
-
const invalid = isLeak && (neverSettled || abandonedNothing);
|
|
92
|
-
return {
|
|
93
|
-
level: warnings.length === 0 ? "high" : "low",
|
|
94
|
-
warnings,
|
|
95
|
-
...invalid && { supersededVerdict: "inconclusive" }
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export {
|
|
100
|
-
effectiveVerdict,
|
|
101
|
-
warrantsIssueDraft,
|
|
102
|
-
assessConfidence
|
|
103
|
-
};
|