skannr 0.1.2 ā 0.1.4
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/.eslintrc.js +22 -22
- package/README.md +260 -151
- package/dist/agent-cli.js +25 -25
- package/dist/agent.js +4 -4
- package/dist/blast-radius.d.ts +80 -0
- package/dist/blast-radius.d.ts.map +1 -0
- package/dist/blast-radius.js +545 -0
- package/dist/blast-radius.js.map +1 -0
- package/dist/cli.js +55 -17
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +98 -24
- package/dist/mcp-server.js.map +1 -1
- package/dist/telemetry.js +12 -12
- package/gemini-extension/GEMINI.md +26 -26
- package/gemini-extension/gemini-extension.json +12 -12
- package/gemini-extension/package.json +14 -14
- package/gemini-extension/src/server.ts +69 -69
- package/gemini-extension/tsconfig.json +14 -14
- package/jest.config.js +4 -4
- package/package.json +54 -53
- package/src/agent-cli.ts +397 -397
- package/src/agent.ts +344 -344
- package/src/benchmark.ts +389 -389
- package/src/blast-radius.ts +661 -0
- package/src/cache.ts +317 -317
- package/src/cli.ts +461 -415
- package/src/config.ts +22 -22
- package/src/formatter.ts +105 -105
- package/src/index.ts +223 -223
- package/src/languages/GenericAdapter.ts +44 -44
- package/src/languages/LanguageAdapter.ts +21 -21
- package/src/languages/PythonAdapter.ts +285 -285
- package/src/languages/TypeScriptAdapter.ts +453 -453
- package/src/languages/registry.ts +49 -49
- package/src/mapper.ts +455 -455
- package/src/mcp-cli.ts +8 -8
- package/src/mcp-server.ts +151 -103
- package/src/ranker-enhanced.ts +514 -514
- package/src/ranker.ts +110 -110
- package/src/scanner.ts +226 -226
- package/src/skeletonizer.ts +22 -22
- package/src/telemetry.ts +146 -146
- package/src/tokenizer.ts +20 -20
- package/src/types.ts +92 -92
- package/src/watcher.ts +119 -119
- package/src/why.ts +88 -88
- package/tests/agent.tools.test.ts +81 -81
- package/tests/benchmark.test.ts +30 -30
- package/tests/blast-radius.test.ts +290 -0
- package/tests/fixtures/sample.py +17 -17
- package/tests/fixtures/sample.ts +13 -13
- package/tests/fixtures/src/api/routes.ts +1 -1
- package/tests/fixtures/src/auth/permission.ts +3 -3
- package/tests/python-adapter.test.ts +31 -31
- package/tests/ranker-enhanced.test.ts +70 -70
- package/tests/ranker.test.ts +79 -79
- package/tests/scanner.scope.test.ts +66 -66
- package/tests/setup-fixtures.js +28 -28
- package/tests/skeletonizer.test.ts +149 -149
- package/tsconfig.json +21 -21
- package/uca-landing/index.html +17 -17
- package/uca-landing/package.json +23 -23
- package/uca-landing/postcss.config.js +6 -6
- package/uca-landing/src/App.jsx +43 -43
- package/uca-landing/src/components/AgentMode.jsx +45 -45
- package/uca-landing/src/components/CliReference.jsx +58 -58
- package/uca-landing/src/components/Features.jsx +82 -82
- package/uca-landing/src/components/Footer.jsx +35 -35
- package/uca-landing/src/components/Hero.jsx +125 -125
- package/uca-landing/src/components/HowItWorks.jsx +60 -60
- package/uca-landing/src/components/Install.jsx +103 -103
- package/uca-landing/src/components/LanguageSupport.jsx +63 -63
- package/uca-landing/src/components/Navbar.jsx +86 -86
- package/uca-landing/src/components/Problem.jsx +51 -51
- package/uca-landing/src/components/Reveal.jsx +40 -40
- package/uca-landing/src/components/WorksWith.jsx +59 -59
- package/uca-landing/src/hooks/useScrollNav.js +13 -13
- package/uca-landing/src/hooks/useTypewriter.js +41 -41
- package/uca-landing/src/index.css +13 -13
- package/uca-landing/src/main.jsx +10 -10
- package/uca-landing/tailwind.config.js +68 -68
- package/uca-landing/vercel.json +3 -3
- package/uca-landing/vite.config.js +6 -6
- package/dist/rocket-chat-scope.d.ts +0 -7
- package/dist/rocket-chat-scope.d.ts.map +0 -1
- package/dist/rocket-chat-scope.js +0 -95
- package/dist/rocket-chat-scope.js.map +0 -1
package/src/benchmark.ts
CHANGED
|
@@ -1,389 +1,389 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Benchmark script for code-analyzer
|
|
3
|
-
* Runs against a repository root to measure token reduction.
|
|
4
|
-
* Compares full-scan token cost vs skeletonized analysis across representative queries.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { analyzeProject } from './index';
|
|
8
|
-
import { scanTypeScriptFiles } from './scanner';
|
|
9
|
-
import { countTokens } from './tokenizer';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
|
|
13
|
-
interface BenchmarkScenario {
|
|
14
|
-
moduleKey: string;
|
|
15
|
-
query: string;
|
|
16
|
-
limit: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface BenchmarkResult {
|
|
20
|
-
projectName: string;
|
|
21
|
-
projectRoot: string;
|
|
22
|
-
query: string;
|
|
23
|
-
// Baseline: what an LLM would consume reading ALL files
|
|
24
|
-
allFilesCount: number;
|
|
25
|
-
allFilesTotalTokens: number;
|
|
26
|
-
// Our tool output
|
|
27
|
-
analyzedFileCount: number;
|
|
28
|
-
analyzedOriginalTokens: number;
|
|
29
|
-
analyzedSkeletonTokens: number;
|
|
30
|
-
tokenReductionVsFullScan: number; // skeleton vs full-repo scan
|
|
31
|
-
tokenReductionVsTopN: number; // skeleton vs top-N original files
|
|
32
|
-
top1KeywordCoverage: number;
|
|
33
|
-
avgTopKKeywordCoverage: number;
|
|
34
|
-
pathIntentMatchRate: number;
|
|
35
|
-
directoryDiversity: number;
|
|
36
|
-
enhancedRanking: boolean;
|
|
37
|
-
executionTimeMs: number;
|
|
38
|
-
topFiles: {
|
|
39
|
-
path: string;
|
|
40
|
-
score: number;
|
|
41
|
-
originalTokens: number;
|
|
42
|
-
skeletonTokens: number;
|
|
43
|
-
reductionPct: number;
|
|
44
|
-
}[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function extractQueryTerms(query: string): string[] {
|
|
48
|
-
return query
|
|
49
|
-
.toLowerCase()
|
|
50
|
-
.split(/[^a-z0-9]+/)
|
|
51
|
-
.filter(term => term.length > 2);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function computeKeywordCoverage(pathValue: string, skeleton: string, terms: string[]): number {
|
|
55
|
-
if (terms.length === 0) return 0;
|
|
56
|
-
const haystack = `${pathValue} ${skeleton}`.toLowerCase();
|
|
57
|
-
const hits = terms.filter(term => haystack.includes(term)).length;
|
|
58
|
-
return hits / terms.length;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function computeRankingQuality(
|
|
62
|
-
files: { path: string; skeleton: string }[],
|
|
63
|
-
query: string
|
|
64
|
-
): {
|
|
65
|
-
top1KeywordCoverage: number;
|
|
66
|
-
avgTopKKeywordCoverage: number;
|
|
67
|
-
pathIntentMatchRate: number;
|
|
68
|
-
directoryDiversity: number;
|
|
69
|
-
} {
|
|
70
|
-
if (files.length === 0) {
|
|
71
|
-
return {
|
|
72
|
-
top1KeywordCoverage: 0,
|
|
73
|
-
avgTopKKeywordCoverage: 0,
|
|
74
|
-
pathIntentMatchRate: 0,
|
|
75
|
-
directoryDiversity: 0,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const terms = extractQueryTerms(query);
|
|
80
|
-
const topK = files.slice(0, Math.min(5, files.length));
|
|
81
|
-
const coverages = topK.map(file => computeKeywordCoverage(file.path, file.skeleton, terms));
|
|
82
|
-
|
|
83
|
-
const top1KeywordCoverage = coverages[0] ?? 0;
|
|
84
|
-
const avgTopKKeywordCoverage = coverages.reduce((sum, value) => sum + value, 0) / topK.length;
|
|
85
|
-
|
|
86
|
-
const pathIntentMatches = topK.filter(file =>
|
|
87
|
-
terms.some(term => file.path.toLowerCase().includes(term))
|
|
88
|
-
).length;
|
|
89
|
-
const pathIntentMatchRate = pathIntentMatches / topK.length;
|
|
90
|
-
|
|
91
|
-
const uniqueDirs = new Set(
|
|
92
|
-
topK.map(file => path.dirname(file.path).toLowerCase())
|
|
93
|
-
);
|
|
94
|
-
const directoryDiversity = uniqueDirs.size / topK.length;
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
top1KeywordCoverage,
|
|
98
|
-
avgTopKKeywordCoverage,
|
|
99
|
-
pathIntentMatchRate,
|
|
100
|
-
directoryDiversity,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const BENCHMARK_SCENARIOS: BenchmarkScenario[] = [
|
|
105
|
-
{ moduleKey: 'src', query: 'authentication flow and permission checks', limit: 10 },
|
|
106
|
-
{ moduleKey: 'lib', query: 'database connection management', limit: 10 },
|
|
107
|
-
{ moduleKey: 'packages', query: 'API endpoint and routing structure', limit: 10 },
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Quickly sum tokens across all .ts/.tsx files in a directory.
|
|
112
|
-
* Uses a capped sample to stay fast, then extrapolates.
|
|
113
|
-
*/
|
|
114
|
-
function sampleDirectoryTokens(
|
|
115
|
-
rootPath: string,
|
|
116
|
-
maxFiles = 50
|
|
117
|
-
): { fileCount: number; totalTokens: number; isSampled: boolean } {
|
|
118
|
-
const allFiles = scanTypeScriptFiles(rootPath);
|
|
119
|
-
const isSampled = allFiles.length > maxFiles;
|
|
120
|
-
const filesToRead = isSampled ? allFiles.slice(0, maxFiles) : allFiles;
|
|
121
|
-
|
|
122
|
-
let totalTokens = 0;
|
|
123
|
-
for (const f of filesToRead) {
|
|
124
|
-
try {
|
|
125
|
-
const content = fs.readFileSync(f, 'utf-8');
|
|
126
|
-
totalTokens += countTokens(content);
|
|
127
|
-
} catch {
|
|
128
|
-
// skip unreadable
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const estimatedTotal = isSampled
|
|
133
|
-
? Math.round((totalTokens / filesToRead.length) * allFiles.length)
|
|
134
|
-
: totalTokens;
|
|
135
|
-
|
|
136
|
-
return { fileCount: allFiles.length, totalTokens: estimatedTotal, isSampled };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Run a single benchmark scenario
|
|
141
|
-
*/
|
|
142
|
-
async function benchmarkProject(
|
|
143
|
-
appRoot: string,
|
|
144
|
-
moduleKey: string,
|
|
145
|
-
projectName: string,
|
|
146
|
-
query: string,
|
|
147
|
-
options: { limit?: number; enhancedRanking?: boolean } = {}
|
|
148
|
-
): Promise<BenchmarkResult> {
|
|
149
|
-
const limit = options.limit ?? 10;
|
|
150
|
-
const moduleFullPath = fs.existsSync(path.join(appRoot, moduleKey))
|
|
151
|
-
? path.join(appRoot, moduleKey)
|
|
152
|
-
: appRoot;
|
|
153
|
-
|
|
154
|
-
// Baseline: estimate full-module token cost (what we are comparing against)
|
|
155
|
-
const baseline = sampleDirectoryTokens(moduleFullPath, 200);
|
|
156
|
-
|
|
157
|
-
const start = Date.now();
|
|
158
|
-
const result = await analyzeProject({
|
|
159
|
-
root: appRoot,
|
|
160
|
-
question: query,
|
|
161
|
-
limit,
|
|
162
|
-
enhancedRanking: options.enhancedRanking ?? true,
|
|
163
|
-
moduleKeys: [moduleKey],
|
|
164
|
-
generateMapping: false,
|
|
165
|
-
});
|
|
166
|
-
const executionTimeMs = Date.now() - start;
|
|
167
|
-
|
|
168
|
-
let analyzedOriginal = 0;
|
|
169
|
-
let analyzedSkeleton = 0;
|
|
170
|
-
|
|
171
|
-
const topFiles = result.files.map(f => {
|
|
172
|
-
analyzedOriginal += f.originalTokenCount;
|
|
173
|
-
analyzedSkeleton += f.skeletonTokenCount;
|
|
174
|
-
return {
|
|
175
|
-
path: f.path,
|
|
176
|
-
score: f.score,
|
|
177
|
-
originalTokens: f.originalTokenCount,
|
|
178
|
-
skeletonTokens: f.skeletonTokenCount,
|
|
179
|
-
reductionPct:
|
|
180
|
-
f.originalTokenCount > 0
|
|
181
|
-
? ((f.originalTokenCount - f.skeletonTokenCount) / f.originalTokenCount) * 100
|
|
182
|
-
: 0,
|
|
183
|
-
};
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const tokenReductionVsFullScan =
|
|
187
|
-
baseline.totalTokens > 0
|
|
188
|
-
? ((baseline.totalTokens - analyzedSkeleton) / baseline.totalTokens) * 100
|
|
189
|
-
: 0;
|
|
190
|
-
|
|
191
|
-
const tokenReductionVsTopN =
|
|
192
|
-
analyzedOriginal > 0
|
|
193
|
-
? ((analyzedOriginal - analyzedSkeleton) / analyzedOriginal) * 100
|
|
194
|
-
: 0;
|
|
195
|
-
|
|
196
|
-
const rankingQuality = computeRankingQuality(
|
|
197
|
-
result.files.map(file => ({ path: file.path, skeleton: file.skeleton })),
|
|
198
|
-
query
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
projectName,
|
|
203
|
-
projectRoot: moduleFullPath,
|
|
204
|
-
query,
|
|
205
|
-
allFilesCount: baseline.fileCount,
|
|
206
|
-
allFilesTotalTokens: baseline.totalTokens,
|
|
207
|
-
analyzedFileCount: result.files.length,
|
|
208
|
-
analyzedOriginalTokens: analyzedOriginal,
|
|
209
|
-
analyzedSkeletonTokens: analyzedSkeleton,
|
|
210
|
-
tokenReductionVsFullScan,
|
|
211
|
-
tokenReductionVsTopN,
|
|
212
|
-
top1KeywordCoverage: rankingQuality.top1KeywordCoverage,
|
|
213
|
-
avgTopKKeywordCoverage: rankingQuality.avgTopKKeywordCoverage,
|
|
214
|
-
pathIntentMatchRate: rankingQuality.pathIntentMatchRate,
|
|
215
|
-
directoryDiversity: rankingQuality.directoryDiversity,
|
|
216
|
-
enhancedRanking: options.enhancedRanking ?? true,
|
|
217
|
-
executionTimeMs,
|
|
218
|
-
topFiles,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Format results for console output
|
|
224
|
-
*/
|
|
225
|
-
function formatResults(results: BenchmarkResult[]): string {
|
|
226
|
-
const lines: string[] = [];
|
|
227
|
-
|
|
228
|
-
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
229
|
-
lines.push(' CODE ANALYZER BENCHMARK ā Generic Repository');
|
|
230
|
-
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
231
|
-
|
|
232
|
-
for (const r of results) {
|
|
233
|
-
lines.push(`\nš ${r.projectName}`);
|
|
234
|
-
lines.push(` Query: "${r.query}"`);
|
|
235
|
-
lines.push('');
|
|
236
|
-
lines.push(` š Repository Baseline (naive full-scan):`);
|
|
237
|
-
lines.push(` Total .ts/.tsx files : ${r.allFilesCount.toLocaleString()}`);
|
|
238
|
-
lines.push(` Estimated tokens : ${r.allFilesTotalTokens.toLocaleString()}`);
|
|
239
|
-
lines.push('');
|
|
240
|
-
lines.push(` āļø Our Tool (top ${r.analyzedFileCount} files, enhanced ranking: ${r.enhancedRanking ? 'ā' : 'ā'}):`);
|
|
241
|
-
lines.push(` Original tokens sent : ${r.analyzedOriginalTokens.toLocaleString()}`);
|
|
242
|
-
lines.push(` Skeleton tokens sent : ${r.analyzedSkeletonTokens.toLocaleString()}`);
|
|
243
|
-
lines.push(` Reduction vs full scan : ${r.tokenReductionVsFullScan.toFixed(1)}% ā¬ļø`);
|
|
244
|
-
lines.push(` Reduction vs top-N raw : ${r.tokenReductionVsTopN.toFixed(1)}% ā¬ļø`);
|
|
245
|
-
lines.push(` Top-1 keyword coverage : ${(r.top1KeywordCoverage * 100).toFixed(1)}%`);
|
|
246
|
-
lines.push(` Avg top-K coverage : ${(r.avgTopKKeywordCoverage * 100).toFixed(1)}%`);
|
|
247
|
-
lines.push(` Path intent match rate : ${(r.pathIntentMatchRate * 100).toFixed(1)}%`);
|
|
248
|
-
lines.push(` Directory diversity : ${(r.directoryDiversity * 100).toFixed(1)}%`);
|
|
249
|
-
lines.push(` Execution time : ${r.executionTimeMs}ms`);
|
|
250
|
-
lines.push('');
|
|
251
|
-
lines.push(` š Top Ranked Files:`);
|
|
252
|
-
for (let i = 0; i < Math.min(5, r.topFiles.length); i++) {
|
|
253
|
-
const f = r.topFiles[i];
|
|
254
|
-
lines.push(
|
|
255
|
-
` ${i + 1}. ${f.path}`
|
|
256
|
-
);
|
|
257
|
-
lines.push(
|
|
258
|
-
` Score: ${f.score.toFixed(3)} | ${f.originalTokens} ā ${f.skeletonTokens} tokens (${f.reductionPct.toFixed(1)}% ā¬ļø)`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
lines.push('');
|
|
262
|
-
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Summary
|
|
266
|
-
const avgReductionFullScan =
|
|
267
|
-
results.reduce((s, r) => s + r.tokenReductionVsFullScan, 0) / results.length;
|
|
268
|
-
const avgReductionTopN =
|
|
269
|
-
results.reduce((s, r) => s + r.tokenReductionVsTopN, 0) / results.length;
|
|
270
|
-
const avgTop1Coverage =
|
|
271
|
-
results.reduce((s, r) => s + r.top1KeywordCoverage, 0) / results.length;
|
|
272
|
-
const avgTopKCoverage =
|
|
273
|
-
results.reduce((s, r) => s + r.avgTopKKeywordCoverage, 0) / results.length;
|
|
274
|
-
const avgPathIntentMatch =
|
|
275
|
-
results.reduce((s, r) => s + r.pathIntentMatchRate, 0) / results.length;
|
|
276
|
-
const avgTime =
|
|
277
|
-
results.reduce((s, r) => s + r.executionTimeMs, 0) / results.length;
|
|
278
|
-
|
|
279
|
-
lines.push('\nš SUMMARY');
|
|
280
|
-
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
281
|
-
lines.push(` Benchmarks run : ${results.length}`);
|
|
282
|
-
lines.push(` Avg reduction vs full scan : ${avgReductionFullScan.toFixed(1)}%`);
|
|
283
|
-
lines.push(` Avg reduction vs top-N raw : ${avgReductionTopN.toFixed(1)}%`);
|
|
284
|
-
lines.push(` Avg top-1 coverage : ${(avgTop1Coverage * 100).toFixed(1)}%`);
|
|
285
|
-
lines.push(` Avg top-K coverage : ${(avgTopKCoverage * 100).toFixed(1)}%`);
|
|
286
|
-
lines.push(` Avg path intent match rate : ${(avgPathIntentMatch * 100).toFixed(1)}%`);
|
|
287
|
-
lines.push(` Avg execution time : ${avgTime.toFixed(0)}ms`);
|
|
288
|
-
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
289
|
-
|
|
290
|
-
return lines.join('\n');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Save benchmark output to JSON
|
|
295
|
-
*/
|
|
296
|
-
function saveResults(results: BenchmarkResult[], outputPath: string): void {
|
|
297
|
-
const out = {
|
|
298
|
-
generatedAt: new Date().toISOString(),
|
|
299
|
-
root: results[0]?.projectRoot ?? '',
|
|
300
|
-
summary: {
|
|
301
|
-
avgReductionVsFullScan:
|
|
302
|
-
results.reduce((s, r) => s + r.tokenReductionVsFullScan, 0) / results.length,
|
|
303
|
-
avgReductionVsTopN:
|
|
304
|
-
results.reduce((s, r) => s + r.tokenReductionVsTopN, 0) / results.length,
|
|
305
|
-
avgTop1KeywordCoverage:
|
|
306
|
-
results.reduce((s, r) => s + r.top1KeywordCoverage, 0) / results.length,
|
|
307
|
-
avgTopKKeywordCoverage:
|
|
308
|
-
results.reduce((s, r) => s + r.avgTopKKeywordCoverage, 0) / results.length,
|
|
309
|
-
avgPathIntentMatchRate:
|
|
310
|
-
results.reduce((s, r) => s + r.pathIntentMatchRate, 0) / results.length,
|
|
311
|
-
avgExecutionTimeMs:
|
|
312
|
-
results.reduce((s, r) => s + r.executionTimeMs, 0) / results.length,
|
|
313
|
-
},
|
|
314
|
-
benchmarks: results,
|
|
315
|
-
};
|
|
316
|
-
fs.writeFileSync(outputPath, JSON.stringify(out, null, 2), 'utf-8');
|
|
317
|
-
console.log(`\nā
Results saved ā ${outputPath}`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Main entry point
|
|
322
|
-
*/
|
|
323
|
-
async function runBenchmarks(): Promise<void> {
|
|
324
|
-
const repoRoot = process.cwd();
|
|
325
|
-
|
|
326
|
-
// Verify repo exists
|
|
327
|
-
if (!fs.existsSync(repoRoot)) {
|
|
328
|
-
console.error(`ā Repository not found at ${repoRoot}`);
|
|
329
|
-
process.exit(1);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const appRoot = repoRoot;
|
|
333
|
-
console.log('š Code Analyzer ā Generic Repository Benchmarks\n');
|
|
334
|
-
console.log(` Repo : ${repoRoot}`);
|
|
335
|
-
console.log(' Computing baseline (first 50 files sample)...');
|
|
336
|
-
const fullBaseline = sampleDirectoryTokens(appRoot, 50);
|
|
337
|
-
console.log(` Baseline ā ${fullBaseline.fileCount} .ts files, ` +
|
|
338
|
-
`~${fullBaseline.totalTokens.toLocaleString()} tokens (${fullBaseline.isSampled ? 'extrapolated' : 'exact'})\n`);
|
|
339
|
-
|
|
340
|
-
const results: BenchmarkResult[] = [];
|
|
341
|
-
|
|
342
|
-
let i = 1;
|
|
343
|
-
for (const scenario of BENCHMARK_SCENARIOS) {
|
|
344
|
-
console.log(`ā¶ [${i}/${BENCHMARK_SCENARIOS.length}] ${scenario.query} (${scenario.moduleKey})`);
|
|
345
|
-
results.push(await benchmarkProject(
|
|
346
|
-
repoRoot,
|
|
347
|
-
scenario.moduleKey,
|
|
348
|
-
`Repo āŗ ${scenario.moduleKey}`,
|
|
349
|
-
scenario.query,
|
|
350
|
-
{ limit: scenario.limit, enhancedRanking: true }
|
|
351
|
-
));
|
|
352
|
-
console.log(` ā ${results.at(-1)!.executionTimeMs}ms\n`);
|
|
353
|
-
i++;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// āā Output āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
357
|
-
console.log(formatResults(results));
|
|
358
|
-
|
|
359
|
-
// Append full-app baseline note to the formatted output
|
|
360
|
-
console.log(`ā¹ļø Full app/ baseline (naive full-scan):`);
|
|
361
|
-
console.log(` ${fullBaseline.fileCount} files ā ~${fullBaseline.totalTokens.toLocaleString()} tokens`);
|
|
362
|
-
console.log(` vs our tool avg skeleton: ~${Math.round(results.reduce((s, r) => s + r.analyzedSkeletonTokens, 0) / results.length).toLocaleString()} tokens per query\n`);
|
|
363
|
-
|
|
364
|
-
const outputPath = path.join(__dirname, '..', 'benchmark-results.json');
|
|
365
|
-
// Attach baseline to JSON output
|
|
366
|
-
const extendedResults = {
|
|
367
|
-
fullAppBaseline: {
|
|
368
|
-
root: appRoot,
|
|
369
|
-
fileCount: fullBaseline.fileCount,
|
|
370
|
-
estimatedTotalTokens: fullBaseline.totalTokens,
|
|
371
|
-
isSampled: fullBaseline.isSampled,
|
|
372
|
-
},
|
|
373
|
-
benchmarks: results,
|
|
374
|
-
};
|
|
375
|
-
fs.writeFileSync(outputPath, JSON.stringify(extendedResults, null, 2), 'utf-8');
|
|
376
|
-
console.log(`ā
Saved ā ${outputPath}`);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// āā Run āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
380
|
-
if (require.main === module) {
|
|
381
|
-
runBenchmarks()
|
|
382
|
-
.then(() => process.exit(0))
|
|
383
|
-
.catch(err => {
|
|
384
|
-
console.error('ā Benchmark failed:', err);
|
|
385
|
-
process.exit(1);
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export { benchmarkProject, formatResults, saveResults };
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark script for code-analyzer
|
|
3
|
+
* Runs against a repository root to measure token reduction.
|
|
4
|
+
* Compares full-scan token cost vs skeletonized analysis across representative queries.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { analyzeProject } from './index';
|
|
8
|
+
import { scanTypeScriptFiles } from './scanner';
|
|
9
|
+
import { countTokens } from './tokenizer';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
|
|
13
|
+
interface BenchmarkScenario {
|
|
14
|
+
moduleKey: string;
|
|
15
|
+
query: string;
|
|
16
|
+
limit: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface BenchmarkResult {
|
|
20
|
+
projectName: string;
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
query: string;
|
|
23
|
+
// Baseline: what an LLM would consume reading ALL files
|
|
24
|
+
allFilesCount: number;
|
|
25
|
+
allFilesTotalTokens: number;
|
|
26
|
+
// Our tool output
|
|
27
|
+
analyzedFileCount: number;
|
|
28
|
+
analyzedOriginalTokens: number;
|
|
29
|
+
analyzedSkeletonTokens: number;
|
|
30
|
+
tokenReductionVsFullScan: number; // skeleton vs full-repo scan
|
|
31
|
+
tokenReductionVsTopN: number; // skeleton vs top-N original files
|
|
32
|
+
top1KeywordCoverage: number;
|
|
33
|
+
avgTopKKeywordCoverage: number;
|
|
34
|
+
pathIntentMatchRate: number;
|
|
35
|
+
directoryDiversity: number;
|
|
36
|
+
enhancedRanking: boolean;
|
|
37
|
+
executionTimeMs: number;
|
|
38
|
+
topFiles: {
|
|
39
|
+
path: string;
|
|
40
|
+
score: number;
|
|
41
|
+
originalTokens: number;
|
|
42
|
+
skeletonTokens: number;
|
|
43
|
+
reductionPct: number;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractQueryTerms(query: string): string[] {
|
|
48
|
+
return query
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.split(/[^a-z0-9]+/)
|
|
51
|
+
.filter(term => term.length > 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function computeKeywordCoverage(pathValue: string, skeleton: string, terms: string[]): number {
|
|
55
|
+
if (terms.length === 0) return 0;
|
|
56
|
+
const haystack = `${pathValue} ${skeleton}`.toLowerCase();
|
|
57
|
+
const hits = terms.filter(term => haystack.includes(term)).length;
|
|
58
|
+
return hits / terms.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function computeRankingQuality(
|
|
62
|
+
files: { path: string; skeleton: string }[],
|
|
63
|
+
query: string
|
|
64
|
+
): {
|
|
65
|
+
top1KeywordCoverage: number;
|
|
66
|
+
avgTopKKeywordCoverage: number;
|
|
67
|
+
pathIntentMatchRate: number;
|
|
68
|
+
directoryDiversity: number;
|
|
69
|
+
} {
|
|
70
|
+
if (files.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
top1KeywordCoverage: 0,
|
|
73
|
+
avgTopKKeywordCoverage: 0,
|
|
74
|
+
pathIntentMatchRate: 0,
|
|
75
|
+
directoryDiversity: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const terms = extractQueryTerms(query);
|
|
80
|
+
const topK = files.slice(0, Math.min(5, files.length));
|
|
81
|
+
const coverages = topK.map(file => computeKeywordCoverage(file.path, file.skeleton, terms));
|
|
82
|
+
|
|
83
|
+
const top1KeywordCoverage = coverages[0] ?? 0;
|
|
84
|
+
const avgTopKKeywordCoverage = coverages.reduce((sum, value) => sum + value, 0) / topK.length;
|
|
85
|
+
|
|
86
|
+
const pathIntentMatches = topK.filter(file =>
|
|
87
|
+
terms.some(term => file.path.toLowerCase().includes(term))
|
|
88
|
+
).length;
|
|
89
|
+
const pathIntentMatchRate = pathIntentMatches / topK.length;
|
|
90
|
+
|
|
91
|
+
const uniqueDirs = new Set(
|
|
92
|
+
topK.map(file => path.dirname(file.path).toLowerCase())
|
|
93
|
+
);
|
|
94
|
+
const directoryDiversity = uniqueDirs.size / topK.length;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
top1KeywordCoverage,
|
|
98
|
+
avgTopKKeywordCoverage,
|
|
99
|
+
pathIntentMatchRate,
|
|
100
|
+
directoryDiversity,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const BENCHMARK_SCENARIOS: BenchmarkScenario[] = [
|
|
105
|
+
{ moduleKey: 'src', query: 'authentication flow and permission checks', limit: 10 },
|
|
106
|
+
{ moduleKey: 'lib', query: 'database connection management', limit: 10 },
|
|
107
|
+
{ moduleKey: 'packages', query: 'API endpoint and routing structure', limit: 10 },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Quickly sum tokens across all .ts/.tsx files in a directory.
|
|
112
|
+
* Uses a capped sample to stay fast, then extrapolates.
|
|
113
|
+
*/
|
|
114
|
+
function sampleDirectoryTokens(
|
|
115
|
+
rootPath: string,
|
|
116
|
+
maxFiles = 50
|
|
117
|
+
): { fileCount: number; totalTokens: number; isSampled: boolean } {
|
|
118
|
+
const allFiles = scanTypeScriptFiles(rootPath);
|
|
119
|
+
const isSampled = allFiles.length > maxFiles;
|
|
120
|
+
const filesToRead = isSampled ? allFiles.slice(0, maxFiles) : allFiles;
|
|
121
|
+
|
|
122
|
+
let totalTokens = 0;
|
|
123
|
+
for (const f of filesToRead) {
|
|
124
|
+
try {
|
|
125
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
126
|
+
totalTokens += countTokens(content);
|
|
127
|
+
} catch {
|
|
128
|
+
// skip unreadable
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const estimatedTotal = isSampled
|
|
133
|
+
? Math.round((totalTokens / filesToRead.length) * allFiles.length)
|
|
134
|
+
: totalTokens;
|
|
135
|
+
|
|
136
|
+
return { fileCount: allFiles.length, totalTokens: estimatedTotal, isSampled };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Run a single benchmark scenario
|
|
141
|
+
*/
|
|
142
|
+
async function benchmarkProject(
|
|
143
|
+
appRoot: string,
|
|
144
|
+
moduleKey: string,
|
|
145
|
+
projectName: string,
|
|
146
|
+
query: string,
|
|
147
|
+
options: { limit?: number; enhancedRanking?: boolean } = {}
|
|
148
|
+
): Promise<BenchmarkResult> {
|
|
149
|
+
const limit = options.limit ?? 10;
|
|
150
|
+
const moduleFullPath = fs.existsSync(path.join(appRoot, moduleKey))
|
|
151
|
+
? path.join(appRoot, moduleKey)
|
|
152
|
+
: appRoot;
|
|
153
|
+
|
|
154
|
+
// Baseline: estimate full-module token cost (what we are comparing against)
|
|
155
|
+
const baseline = sampleDirectoryTokens(moduleFullPath, 200);
|
|
156
|
+
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
const result = await analyzeProject({
|
|
159
|
+
root: appRoot,
|
|
160
|
+
question: query,
|
|
161
|
+
limit,
|
|
162
|
+
enhancedRanking: options.enhancedRanking ?? true,
|
|
163
|
+
moduleKeys: [moduleKey],
|
|
164
|
+
generateMapping: false,
|
|
165
|
+
});
|
|
166
|
+
const executionTimeMs = Date.now() - start;
|
|
167
|
+
|
|
168
|
+
let analyzedOriginal = 0;
|
|
169
|
+
let analyzedSkeleton = 0;
|
|
170
|
+
|
|
171
|
+
const topFiles = result.files.map(f => {
|
|
172
|
+
analyzedOriginal += f.originalTokenCount;
|
|
173
|
+
analyzedSkeleton += f.skeletonTokenCount;
|
|
174
|
+
return {
|
|
175
|
+
path: f.path,
|
|
176
|
+
score: f.score,
|
|
177
|
+
originalTokens: f.originalTokenCount,
|
|
178
|
+
skeletonTokens: f.skeletonTokenCount,
|
|
179
|
+
reductionPct:
|
|
180
|
+
f.originalTokenCount > 0
|
|
181
|
+
? ((f.originalTokenCount - f.skeletonTokenCount) / f.originalTokenCount) * 100
|
|
182
|
+
: 0,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const tokenReductionVsFullScan =
|
|
187
|
+
baseline.totalTokens > 0
|
|
188
|
+
? ((baseline.totalTokens - analyzedSkeleton) / baseline.totalTokens) * 100
|
|
189
|
+
: 0;
|
|
190
|
+
|
|
191
|
+
const tokenReductionVsTopN =
|
|
192
|
+
analyzedOriginal > 0
|
|
193
|
+
? ((analyzedOriginal - analyzedSkeleton) / analyzedOriginal) * 100
|
|
194
|
+
: 0;
|
|
195
|
+
|
|
196
|
+
const rankingQuality = computeRankingQuality(
|
|
197
|
+
result.files.map(file => ({ path: file.path, skeleton: file.skeleton })),
|
|
198
|
+
query
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
projectName,
|
|
203
|
+
projectRoot: moduleFullPath,
|
|
204
|
+
query,
|
|
205
|
+
allFilesCount: baseline.fileCount,
|
|
206
|
+
allFilesTotalTokens: baseline.totalTokens,
|
|
207
|
+
analyzedFileCount: result.files.length,
|
|
208
|
+
analyzedOriginalTokens: analyzedOriginal,
|
|
209
|
+
analyzedSkeletonTokens: analyzedSkeleton,
|
|
210
|
+
tokenReductionVsFullScan,
|
|
211
|
+
tokenReductionVsTopN,
|
|
212
|
+
top1KeywordCoverage: rankingQuality.top1KeywordCoverage,
|
|
213
|
+
avgTopKKeywordCoverage: rankingQuality.avgTopKKeywordCoverage,
|
|
214
|
+
pathIntentMatchRate: rankingQuality.pathIntentMatchRate,
|
|
215
|
+
directoryDiversity: rankingQuality.directoryDiversity,
|
|
216
|
+
enhancedRanking: options.enhancedRanking ?? true,
|
|
217
|
+
executionTimeMs,
|
|
218
|
+
topFiles,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Format results for console output
|
|
224
|
+
*/
|
|
225
|
+
function formatResults(results: BenchmarkResult[]): string {
|
|
226
|
+
const lines: string[] = [];
|
|
227
|
+
|
|
228
|
+
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
229
|
+
lines.push(' CODE ANALYZER BENCHMARK ā Generic Repository');
|
|
230
|
+
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
231
|
+
|
|
232
|
+
for (const r of results) {
|
|
233
|
+
lines.push(`\nš ${r.projectName}`);
|
|
234
|
+
lines.push(` Query: "${r.query}"`);
|
|
235
|
+
lines.push('');
|
|
236
|
+
lines.push(` š Repository Baseline (naive full-scan):`);
|
|
237
|
+
lines.push(` Total .ts/.tsx files : ${r.allFilesCount.toLocaleString()}`);
|
|
238
|
+
lines.push(` Estimated tokens : ${r.allFilesTotalTokens.toLocaleString()}`);
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push(` āļø Our Tool (top ${r.analyzedFileCount} files, enhanced ranking: ${r.enhancedRanking ? 'ā' : 'ā'}):`);
|
|
241
|
+
lines.push(` Original tokens sent : ${r.analyzedOriginalTokens.toLocaleString()}`);
|
|
242
|
+
lines.push(` Skeleton tokens sent : ${r.analyzedSkeletonTokens.toLocaleString()}`);
|
|
243
|
+
lines.push(` Reduction vs full scan : ${r.tokenReductionVsFullScan.toFixed(1)}% ā¬ļø`);
|
|
244
|
+
lines.push(` Reduction vs top-N raw : ${r.tokenReductionVsTopN.toFixed(1)}% ā¬ļø`);
|
|
245
|
+
lines.push(` Top-1 keyword coverage : ${(r.top1KeywordCoverage * 100).toFixed(1)}%`);
|
|
246
|
+
lines.push(` Avg top-K coverage : ${(r.avgTopKKeywordCoverage * 100).toFixed(1)}%`);
|
|
247
|
+
lines.push(` Path intent match rate : ${(r.pathIntentMatchRate * 100).toFixed(1)}%`);
|
|
248
|
+
lines.push(` Directory diversity : ${(r.directoryDiversity * 100).toFixed(1)}%`);
|
|
249
|
+
lines.push(` Execution time : ${r.executionTimeMs}ms`);
|
|
250
|
+
lines.push('');
|
|
251
|
+
lines.push(` š Top Ranked Files:`);
|
|
252
|
+
for (let i = 0; i < Math.min(5, r.topFiles.length); i++) {
|
|
253
|
+
const f = r.topFiles[i];
|
|
254
|
+
lines.push(
|
|
255
|
+
` ${i + 1}. ${f.path}`
|
|
256
|
+
);
|
|
257
|
+
lines.push(
|
|
258
|
+
` Score: ${f.score.toFixed(3)} | ${f.originalTokens} ā ${f.skeletonTokens} tokens (${f.reductionPct.toFixed(1)}% ā¬ļø)`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Summary
|
|
266
|
+
const avgReductionFullScan =
|
|
267
|
+
results.reduce((s, r) => s + r.tokenReductionVsFullScan, 0) / results.length;
|
|
268
|
+
const avgReductionTopN =
|
|
269
|
+
results.reduce((s, r) => s + r.tokenReductionVsTopN, 0) / results.length;
|
|
270
|
+
const avgTop1Coverage =
|
|
271
|
+
results.reduce((s, r) => s + r.top1KeywordCoverage, 0) / results.length;
|
|
272
|
+
const avgTopKCoverage =
|
|
273
|
+
results.reduce((s, r) => s + r.avgTopKKeywordCoverage, 0) / results.length;
|
|
274
|
+
const avgPathIntentMatch =
|
|
275
|
+
results.reduce((s, r) => s + r.pathIntentMatchRate, 0) / results.length;
|
|
276
|
+
const avgTime =
|
|
277
|
+
results.reduce((s, r) => s + r.executionTimeMs, 0) / results.length;
|
|
278
|
+
|
|
279
|
+
lines.push('\nš SUMMARY');
|
|
280
|
+
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
281
|
+
lines.push(` Benchmarks run : ${results.length}`);
|
|
282
|
+
lines.push(` Avg reduction vs full scan : ${avgReductionFullScan.toFixed(1)}%`);
|
|
283
|
+
lines.push(` Avg reduction vs top-N raw : ${avgReductionTopN.toFixed(1)}%`);
|
|
284
|
+
lines.push(` Avg top-1 coverage : ${(avgTop1Coverage * 100).toFixed(1)}%`);
|
|
285
|
+
lines.push(` Avg top-K coverage : ${(avgTopKCoverage * 100).toFixed(1)}%`);
|
|
286
|
+
lines.push(` Avg path intent match rate : ${(avgPathIntentMatch * 100).toFixed(1)}%`);
|
|
287
|
+
lines.push(` Avg execution time : ${avgTime.toFixed(0)}ms`);
|
|
288
|
+
lines.push('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
289
|
+
|
|
290
|
+
return lines.join('\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Save benchmark output to JSON
|
|
295
|
+
*/
|
|
296
|
+
function saveResults(results: BenchmarkResult[], outputPath: string): void {
|
|
297
|
+
const out = {
|
|
298
|
+
generatedAt: new Date().toISOString(),
|
|
299
|
+
root: results[0]?.projectRoot ?? '',
|
|
300
|
+
summary: {
|
|
301
|
+
avgReductionVsFullScan:
|
|
302
|
+
results.reduce((s, r) => s + r.tokenReductionVsFullScan, 0) / results.length,
|
|
303
|
+
avgReductionVsTopN:
|
|
304
|
+
results.reduce((s, r) => s + r.tokenReductionVsTopN, 0) / results.length,
|
|
305
|
+
avgTop1KeywordCoverage:
|
|
306
|
+
results.reduce((s, r) => s + r.top1KeywordCoverage, 0) / results.length,
|
|
307
|
+
avgTopKKeywordCoverage:
|
|
308
|
+
results.reduce((s, r) => s + r.avgTopKKeywordCoverage, 0) / results.length,
|
|
309
|
+
avgPathIntentMatchRate:
|
|
310
|
+
results.reduce((s, r) => s + r.pathIntentMatchRate, 0) / results.length,
|
|
311
|
+
avgExecutionTimeMs:
|
|
312
|
+
results.reduce((s, r) => s + r.executionTimeMs, 0) / results.length,
|
|
313
|
+
},
|
|
314
|
+
benchmarks: results,
|
|
315
|
+
};
|
|
316
|
+
fs.writeFileSync(outputPath, JSON.stringify(out, null, 2), 'utf-8');
|
|
317
|
+
console.log(`\nā
Results saved ā ${outputPath}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Main entry point
|
|
322
|
+
*/
|
|
323
|
+
async function runBenchmarks(): Promise<void> {
|
|
324
|
+
const repoRoot = process.cwd();
|
|
325
|
+
|
|
326
|
+
// Verify repo exists
|
|
327
|
+
if (!fs.existsSync(repoRoot)) {
|
|
328
|
+
console.error(`ā Repository not found at ${repoRoot}`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const appRoot = repoRoot;
|
|
333
|
+
console.log('š Code Analyzer ā Generic Repository Benchmarks\n');
|
|
334
|
+
console.log(` Repo : ${repoRoot}`);
|
|
335
|
+
console.log(' Computing baseline (first 50 files sample)...');
|
|
336
|
+
const fullBaseline = sampleDirectoryTokens(appRoot, 50);
|
|
337
|
+
console.log(` Baseline ā ${fullBaseline.fileCount} .ts files, ` +
|
|
338
|
+
`~${fullBaseline.totalTokens.toLocaleString()} tokens (${fullBaseline.isSampled ? 'extrapolated' : 'exact'})\n`);
|
|
339
|
+
|
|
340
|
+
const results: BenchmarkResult[] = [];
|
|
341
|
+
|
|
342
|
+
let i = 1;
|
|
343
|
+
for (const scenario of BENCHMARK_SCENARIOS) {
|
|
344
|
+
console.log(`ā¶ [${i}/${BENCHMARK_SCENARIOS.length}] ${scenario.query} (${scenario.moduleKey})`);
|
|
345
|
+
results.push(await benchmarkProject(
|
|
346
|
+
repoRoot,
|
|
347
|
+
scenario.moduleKey,
|
|
348
|
+
`Repo āŗ ${scenario.moduleKey}`,
|
|
349
|
+
scenario.query,
|
|
350
|
+
{ limit: scenario.limit, enhancedRanking: true }
|
|
351
|
+
));
|
|
352
|
+
console.log(` ā ${results.at(-1)!.executionTimeMs}ms\n`);
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// āā Output āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
357
|
+
console.log(formatResults(results));
|
|
358
|
+
|
|
359
|
+
// Append full-app baseline note to the formatted output
|
|
360
|
+
console.log(`ā¹ļø Full app/ baseline (naive full-scan):`);
|
|
361
|
+
console.log(` ${fullBaseline.fileCount} files ā ~${fullBaseline.totalTokens.toLocaleString()} tokens`);
|
|
362
|
+
console.log(` vs our tool avg skeleton: ~${Math.round(results.reduce((s, r) => s + r.analyzedSkeletonTokens, 0) / results.length).toLocaleString()} tokens per query\n`);
|
|
363
|
+
|
|
364
|
+
const outputPath = path.join(__dirname, '..', 'benchmark-results.json');
|
|
365
|
+
// Attach baseline to JSON output
|
|
366
|
+
const extendedResults = {
|
|
367
|
+
fullAppBaseline: {
|
|
368
|
+
root: appRoot,
|
|
369
|
+
fileCount: fullBaseline.fileCount,
|
|
370
|
+
estimatedTotalTokens: fullBaseline.totalTokens,
|
|
371
|
+
isSampled: fullBaseline.isSampled,
|
|
372
|
+
},
|
|
373
|
+
benchmarks: results,
|
|
374
|
+
};
|
|
375
|
+
fs.writeFileSync(outputPath, JSON.stringify(extendedResults, null, 2), 'utf-8');
|
|
376
|
+
console.log(`ā
Saved ā ${outputPath}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// āā Run āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
380
|
+
if (require.main === module) {
|
|
381
|
+
runBenchmarks()
|
|
382
|
+
.then(() => process.exit(0))
|
|
383
|
+
.catch(err => {
|
|
384
|
+
console.error('ā Benchmark failed:', err);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export { benchmarkProject, formatResults, saveResults };
|