Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 11 additions & 18 deletions benchmark/baseline.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
{
"detectionPrecision": 0.3626373626373626,
"detectionRecall": 0.4852941176470588,
"providerAttributionAccuracy": 0.8214285714285714,
"findingPrecision": 0.3333333333333333,
"detectionPrecision": 0.35353535353535354,
"detectionRecall": 0.5147058823529411,
"providerAttributionAccuracy": 0.8275862068965517,
"findingPrecision": 1,
"findingRecall": 0.3333333333333333,
"findingMetricsByType": {
"batch": {
"truePositives": 0,
"falsePositives": 1,
"falseNegatives": 1,
"precision": 0,
"recall": 0
},
"rate_limit": {
"truePositives": 0,
"falsePositives": 1,
"falseNegatives": 0,
"precision": 0,
"recall": 1
},
"n_plus_one": {
"truePositives": 1,
"falsePositives": 0,
"falseNegatives": 0,
"precision": 1,
"recall": 1
},
"batch": {
"truePositives": 0,
"falsePositives": 0,
"falseNegatives": 1,
"precision": 1,
"recall": 0
},
"unbatched_parallel": {
"truePositives": 0,
"falsePositives": 0,
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@
"build:webview": "cd webview && npm run build",
"build:dashboard": "cd dashboard && npm run build && rm -rf ../dashboard-dist && cp -r dist ../dashboard-dist",
"test": "npm run test:scanner",
"test:scanner": "tsc -p tsconfig.scanner-tests.json && tsc -p tsconfig.benchmark.json && node dist-test/test/scanner-patterns.test.js && node dist-test/test/workspace-scanner.test.js && node dist-test/test/workspace-file-access.test.js && node dist-test/test/endpoint-classification.test.js && node dist-test/test/local-waste-detector.test.js && node dist-test/test/chat-providers.test.js && node dist-test/test/fingerprint-registry.test.js && node dist-test/test/pricing-sync.test.js && node dist-test/test/ast-parser-loader.test.js && node dist-test/test/ast-call-visitor.test.js && node dist-test/test/ast-import-resolver.test.js && node dist-test/test/ast-scanner.test.js && node dist-test/test/ast-python.test.js && node dist-test/test/ast-frequency-analyzer.test.js && node dist-test/test/ast-cache-detector.test.js && node dist-test/test/ast-batch-detector.test.js && node dist-test/test/ast-concurrency-detector.test.js && node dist-test/test/ast-cross-file-resolver.test.js && node dist-test/test/a1-multi-hop-wrappers.test.js && node dist-test/intelligence/__tests__/builder.test.js && node dist-test/intelligence/__tests__/clusters.test.js && node dist-test/intelligence/__tests__/compression.test.js && node dist-test/intelligence/__tests__/export.test.js && node dist-test/test/api-client.test.js && node dist-test/test/key-management.test.js && node dist-test/test/ast-parser-loader-fallback.test.js && node dist-test/intelligence/__tests__/cost-utils.test.js && node dist-test/test/intelligence-compression-async.test.js && node dist-test/test/webview-provider-dispatch.test.js && node dist-test/test/extension-activation.test.js && node dist-test/test/source-span.test.js && node dist-test/test/url-template.test.js && node dist-test/test/enclosing-function.test.js && node dist-test/test/endpoint-id.test.js && node dist-test/test/parity.test.js && node dist-test/test/a6-object-literal-fps.test.js && node dist-test/test/a2-const-fold.test.js && node dist-test/test/a7-url-path-fallback.test.js && node dist-test/test/c1-pr2-cache-tightening.test.js && node dist-test/test/c1-pr3-batch-tightening.test.js && node dist-test/src/test/benchmark-schema.test.js && node dist-test/src/test/benchmark-metrics.test.js && node dist-test/test/pre-a-scanfiles-resolution.test.js && node dist-test/test/pre-b-export-const-tracking.test.js && node dist-test/test/a3-barrel-reexports.test.js && node dist-test/test/a5-factory-di-aliased.test.js",
"test:scanner": "tsc -p tsconfig.scanner-tests.json && tsc -p tsconfig.benchmark.json && node dist-test/test/scanner-patterns.test.js && node dist-test/test/workspace-scanner.test.js && node dist-test/test/workspace-file-access.test.js && node dist-test/test/endpoint-classification.test.js && node dist-test/test/local-waste-detector.test.js && node dist-test/test/chat-providers.test.js && node dist-test/test/fingerprint-registry.test.js && node dist-test/test/pricing-sync.test.js && node dist-test/test/ast-parser-loader.test.js && node dist-test/test/ast-call-visitor.test.js && node dist-test/test/ast-import-resolver.test.js && node dist-test/test/ast-scanner.test.js && node dist-test/test/ast-python.test.js && node dist-test/test/ast-frequency-analyzer.test.js && node dist-test/test/ast-cache-detector.test.js && node dist-test/test/ast-batch-detector.test.js && node dist-test/test/ast-concurrency-detector.test.js && node dist-test/test/ast-cross-file-resolver.test.js && node dist-test/test/a1-multi-hop-wrappers.test.js && node dist-test/intelligence/__tests__/builder.test.js && node dist-test/intelligence/__tests__/clusters.test.js && node dist-test/intelligence/__tests__/compression.test.js && node dist-test/intelligence/__tests__/export.test.js && node dist-test/test/api-client.test.js && node dist-test/test/key-management.test.js && node dist-test/test/ast-parser-loader-fallback.test.js && node dist-test/intelligence/__tests__/cost-utils.test.js && node dist-test/test/intelligence-compression-async.test.js && node dist-test/test/webview-provider-dispatch.test.js && node dist-test/test/extension-activation.test.js && node dist-test/test/source-span.test.js && node dist-test/test/url-template.test.js && node dist-test/test/enclosing-function.test.js && node dist-test/test/endpoint-id.test.js && node dist-test/test/parity.test.js && node dist-test/test/a6-object-literal-fps.test.js && node dist-test/test/a2-const-fold.test.js && node dist-test/test/a7-url-path-fallback.test.js && node dist-test/test/c1-pr2-cache-tightening.test.js && node dist-test/test/c1-pr3-batch-tightening.test.js && node dist-test/src/test/benchmark-schema.test.js && node dist-test/src/test/benchmark-metrics.test.js && node dist-test/test/c1-pr4-rate-limit-tightening.test.js && node dist-test/test/c1-pr4-batch-residual.test.js && node dist-test/test/pre-a-scanfiles-resolution.test.js && node dist-test/test/pre-b-export-const-tracking.test.js && node dist-test/test/a3-barrel-reexports.test.js && node dist-test/test/a5-factory-di-aliased.test.js",
"calibrate-detectors": "tsc -p tsconfig.scanner-tests.json && node dist-test/test/waste-calibration.js",
"watch:ext": "node esbuild.mjs --watch",
"watch:webview": "cd webview && npm run build -- --watch",
Expand Down
9 changes: 8 additions & 1 deletion src/ast/waste/batch-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,14 @@ function detectSequential(
filePath: string,
isTestLike: boolean
): LocalWasteFinding[] {
const providerMatches = matches.filter(isRealProviderMatch);
// Skip resolver-echoed matches: when cross-file resolution propagates a
// provider attribution onto a project-local wrapper call (e.g.
// `handleApi({path:"/x"})`), two such calls in the same function look like
// duplicate work but are semantically distinct operations dispatched
// through one wrapper. Direct SDK calls (`crossFile` falsy) keep firing.
const providerMatches = matches
.filter(isRealProviderMatch)
.filter((m) => !m.crossFile);
const byBucket = new Map<string, { provider: string; matches: AstCallMatch[] }>();
for (const m of providerMatches) {
if (m.frequency !== "single" || m.loopContext) continue;
Expand Down
4 changes: 2 additions & 2 deletions src/ast/waste/concurrency-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import type { Severity, SuggestionType } from "../../analysis/types";

// ── Guard patterns (source text window ±8 lines) ─────────────────────────────

/** Exponential backoff / retry pacing signals. */
/** Exponential backoff / retry pacing primitives or known retry-wrapper invocations. */
const BACKOFF_GUARD =
/\b(backoff|jitter|retryAfter|exponential|sleep\s*\(|delay\s*\(|retryDelay|retryAfterMs)\b/i;
/\b(?:backoff|jitter|retryAfter|exponential|retryDelay|retryAfterMs)\b|\b(?:sleep|delay|withRetry|with_retry|pRetry|asyncRetry|retryAsync|withBackoff|with_backoff)\s*\(/i;

/** Concurrency limiters / throttles that protect fan-out. */
const CONCURRENCY_GUARD =
Expand Down
42 changes: 42 additions & 0 deletions src/test/c1-pr4-batch-residual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import * as path from "node:path";
import * as fs from "node:fs";
import { setWasmDir } from "../ast/parser-loader";
import { detectLocalWastePatternsInFiles, type ScanFileAccess, type ScanInputFile } from "../scanner/core-scanner";

const WASM_DIR = path.join(__dirname, "..", "..", "assets", "parsers");
setWasmDir(WASM_DIR);

async function run(name: string, fn: () => void | Promise<void>): Promise<void> {
try { await fn(); console.log(`PASS ${name}`); }
catch (err) { console.error(`FAIL ${name}`); throw err; }
}

function buildFixtureAccess(fixtureDir: string, fileNames: string[]): ScanFileAccess {
const files: ScanInputFile[] = fileNames.map((name) => ({
absolutePath: path.join(fixtureDir, name),
relativePath: name,
}));
return {
files,
readFile: async (absolutePath: string) => fs.readFileSync(absolutePath, "utf-8"),
};
}

(async () => {
const projectRoot = path.resolve(__dirname, "..", "..");
const fixtureDir = path.resolve(projectRoot, "src", "test", "fixtures", "c1-pr4");

await run("TS two cross-file wrapper calls in same function do NOT trigger batch finding", async () => {
const access = buildFixtureAccess(fixtureDir, [
"ts_wrapper_sequential_main.ts",
"ts_wrapper_sequential_helper.ts",
]);
const findings = await detectLocalWastePatternsInFiles(access);
const batchFindings = findings.filter((f) => f.type === "batch");
assert.equal(
batchFindings.length, 0,
`expected 0 batch findings on cross-file wrapper sequence, got ${batchFindings.length}: ${JSON.stringify(batchFindings.map((f) => ({ file: f.affectedFile, line: f.line, ev: f.evidence })))}`
);
});
})().catch((err) => { console.error(err); process.exit(1); });
49 changes: 49 additions & 0 deletions src/test/c1-pr4-rate-limit-tightening.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import * as path from "node:path";
import * as fs from "node:fs";
import { setWasmDir } from "../ast/parser-loader";
import { detectLocalWastePatternsInFiles, type ScanFileAccess, type ScanInputFile } from "../scanner/core-scanner";

const WASM_DIR = path.join(__dirname, "..", "..", "assets", "parsers");
setWasmDir(WASM_DIR);

async function run(name: string, fn: () => void | Promise<void>): Promise<void> {
try { await fn(); console.log(`PASS ${name}`); }
catch (err) { console.error(`FAIL ${name}`); throw err; }
}

function buildFixtureAccess(fixtureDir: string, fileNames: string[]): ScanFileAccess {
const files: ScanInputFile[] = fileNames.map((name) => ({
absolutePath: path.join(fixtureDir, name),
relativePath: name,
}));
return {
files,
readFile: async (absolutePath: string) => fs.readFileSync(absolutePath, "utf-8"),
};
}

(async () => {
const projectRoot = path.resolve(__dirname, "..", "..");
const fixtureDir = path.resolve(projectRoot, "src", "test", "fixtures", "c1-pr4");

await run("TS withRetry() wrapper does NOT trigger rate_limit finding", async () => {
const access = buildFixtureAccess(fixtureDir, ["ts_retry_wrapper.ts"]);
const findings = await detectLocalWastePatternsInFiles(access);
const rateLimitFindings = findings.filter((f) => f.type === "rate_limit");
assert.equal(
rateLimitFindings.length, 0,
`expected 0 rate_limit findings around withRetry() wrapper, got ${rateLimitFindings.length}: ${JSON.stringify(rateLimitFindings.map((f) => ({ file: f.affectedFile, line: f.line, ev: f.evidence })))}`
);
});

await run("TS bare retry loop without backoff STILL triggers rate_limit finding", async () => {
const access = buildFixtureAccess(fixtureDir, ["ts_retry_loop.ts"]);
const findings = await detectLocalWastePatternsInFiles(access);
const rateLimitFindings = findings.filter((f) => f.type === "rate_limit");
assert.ok(
rateLimitFindings.length >= 1,
`expected at least 1 rate_limit finding on bare retry loop, got ${rateLimitFindings.length}: full findings = ${JSON.stringify(findings.map((f) => ({ type: f.type, file: f.affectedFile, line: f.line })))}`
);
});
})().catch((err) => { console.error(err); process.exit(1); });
19 changes: 19 additions & 0 deletions src/test/fixtures/c1-pr4/ts_retry_loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import OpenAI from "openai";

const client = new OpenAI();

export async function flakyCompletion(text: string): Promise<string> {
let lastErr: unknown;
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const r = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: text }],
});
return r.choices[0]?.message?.content ?? "";
} catch (err) {
lastErr = err;
}
}
throw lastErr;
}
12 changes: 12 additions & 0 deletions src/test/fixtures/c1-pr4/ts_retry_wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import OpenAI from "openai";
import { withRetry } from "./retry";

const client = new OpenAI();

export async function summarizeShort(text: string): Promise<string> {
const prompt = `Summarize: ${text}`;
return withRetry(() => client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
}));
}
11 changes: 11 additions & 0 deletions src/test/fixtures/c1-pr4/ts_wrapper_sequential_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import OpenAI from "openai";

const client = new OpenAI();

export async function handleApi(arg: { path: string; body: unknown }): Promise<string> {
const r = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: JSON.stringify(arg) }],
});
return r.choices[0]?.message?.content ?? "";
}
15 changes: 15 additions & 0 deletions src/test/fixtures/c1-pr4/ts_wrapper_sequential_main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { handleApi } from "./ts_wrapper_sequential_helper";

export async function main(): Promise<void> {
const summary = await handleApi({
path: "/summarize",
body: { text: "first request" },
});
console.log(summary);

const answer = await handleApi({
path: "/answer",
body: { text: "second request" },
});
console.log(answer);
}
Loading