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.
Files changed (88) hide show
  1. package/.eslintrc.js +22 -22
  2. package/README.md +260 -151
  3. package/dist/agent-cli.js +25 -25
  4. package/dist/agent.js +4 -4
  5. package/dist/blast-radius.d.ts +80 -0
  6. package/dist/blast-radius.d.ts.map +1 -0
  7. package/dist/blast-radius.js +545 -0
  8. package/dist/blast-radius.js.map +1 -0
  9. package/dist/cli.js +55 -17
  10. package/dist/cli.js.map +1 -1
  11. package/dist/mcp-server.d.ts.map +1 -1
  12. package/dist/mcp-server.js +98 -24
  13. package/dist/mcp-server.js.map +1 -1
  14. package/dist/telemetry.js +12 -12
  15. package/gemini-extension/GEMINI.md +26 -26
  16. package/gemini-extension/gemini-extension.json +12 -12
  17. package/gemini-extension/package.json +14 -14
  18. package/gemini-extension/src/server.ts +69 -69
  19. package/gemini-extension/tsconfig.json +14 -14
  20. package/jest.config.js +4 -4
  21. package/package.json +54 -53
  22. package/src/agent-cli.ts +397 -397
  23. package/src/agent.ts +344 -344
  24. package/src/benchmark.ts +389 -389
  25. package/src/blast-radius.ts +661 -0
  26. package/src/cache.ts +317 -317
  27. package/src/cli.ts +461 -415
  28. package/src/config.ts +22 -22
  29. package/src/formatter.ts +105 -105
  30. package/src/index.ts +223 -223
  31. package/src/languages/GenericAdapter.ts +44 -44
  32. package/src/languages/LanguageAdapter.ts +21 -21
  33. package/src/languages/PythonAdapter.ts +285 -285
  34. package/src/languages/TypeScriptAdapter.ts +453 -453
  35. package/src/languages/registry.ts +49 -49
  36. package/src/mapper.ts +455 -455
  37. package/src/mcp-cli.ts +8 -8
  38. package/src/mcp-server.ts +151 -103
  39. package/src/ranker-enhanced.ts +514 -514
  40. package/src/ranker.ts +110 -110
  41. package/src/scanner.ts +226 -226
  42. package/src/skeletonizer.ts +22 -22
  43. package/src/telemetry.ts +146 -146
  44. package/src/tokenizer.ts +20 -20
  45. package/src/types.ts +92 -92
  46. package/src/watcher.ts +119 -119
  47. package/src/why.ts +88 -88
  48. package/tests/agent.tools.test.ts +81 -81
  49. package/tests/benchmark.test.ts +30 -30
  50. package/tests/blast-radius.test.ts +290 -0
  51. package/tests/fixtures/sample.py +17 -17
  52. package/tests/fixtures/sample.ts +13 -13
  53. package/tests/fixtures/src/api/routes.ts +1 -1
  54. package/tests/fixtures/src/auth/permission.ts +3 -3
  55. package/tests/python-adapter.test.ts +31 -31
  56. package/tests/ranker-enhanced.test.ts +70 -70
  57. package/tests/ranker.test.ts +79 -79
  58. package/tests/scanner.scope.test.ts +66 -66
  59. package/tests/setup-fixtures.js +28 -28
  60. package/tests/skeletonizer.test.ts +149 -149
  61. package/tsconfig.json +21 -21
  62. package/uca-landing/index.html +17 -17
  63. package/uca-landing/package.json +23 -23
  64. package/uca-landing/postcss.config.js +6 -6
  65. package/uca-landing/src/App.jsx +43 -43
  66. package/uca-landing/src/components/AgentMode.jsx +45 -45
  67. package/uca-landing/src/components/CliReference.jsx +58 -58
  68. package/uca-landing/src/components/Features.jsx +82 -82
  69. package/uca-landing/src/components/Footer.jsx +35 -35
  70. package/uca-landing/src/components/Hero.jsx +125 -125
  71. package/uca-landing/src/components/HowItWorks.jsx +60 -60
  72. package/uca-landing/src/components/Install.jsx +103 -103
  73. package/uca-landing/src/components/LanguageSupport.jsx +63 -63
  74. package/uca-landing/src/components/Navbar.jsx +86 -86
  75. package/uca-landing/src/components/Problem.jsx +51 -51
  76. package/uca-landing/src/components/Reveal.jsx +40 -40
  77. package/uca-landing/src/components/WorksWith.jsx +59 -59
  78. package/uca-landing/src/hooks/useScrollNav.js +13 -13
  79. package/uca-landing/src/hooks/useTypewriter.js +41 -41
  80. package/uca-landing/src/index.css +13 -13
  81. package/uca-landing/src/main.jsx +10 -10
  82. package/uca-landing/tailwind.config.js +68 -68
  83. package/uca-landing/vercel.json +3 -3
  84. package/uca-landing/vite.config.js +6 -6
  85. package/dist/rocket-chat-scope.d.ts +0 -7
  86. package/dist/rocket-chat-scope.d.ts.map +0 -1
  87. package/dist/rocket-chat-scope.js +0 -95
  88. 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 };