skannr 0.1.1

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 (142) hide show
  1. package/.eslintrc.js +22 -0
  2. package/README.md +109 -0
  3. package/dist/agent-cli.d.ts +7 -0
  4. package/dist/agent-cli.d.ts.map +1 -0
  5. package/dist/agent-cli.js +364 -0
  6. package/dist/agent-cli.js.map +1 -0
  7. package/dist/agent.d.ts +115 -0
  8. package/dist/agent.d.ts.map +1 -0
  9. package/dist/agent.js +340 -0
  10. package/dist/agent.js.map +1 -0
  11. package/dist/benchmark.d.ts +53 -0
  12. package/dist/benchmark.d.ts.map +1 -0
  13. package/dist/benchmark.js +307 -0
  14. package/dist/benchmark.js.map +1 -0
  15. package/dist/cache.d.ts +97 -0
  16. package/dist/cache.d.ts.map +1 -0
  17. package/dist/cache.js +284 -0
  18. package/dist/cache.js.map +1 -0
  19. package/dist/cli.d.ts +6 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +238 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config.d.ts +8 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +52 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +176 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/languages/GenericAdapter.d.ts +10 -0
  32. package/dist/languages/GenericAdapter.d.ts.map +1 -0
  33. package/dist/languages/GenericAdapter.js +41 -0
  34. package/dist/languages/GenericAdapter.js.map +1 -0
  35. package/dist/languages/LanguageAdapter.d.ts +14 -0
  36. package/dist/languages/LanguageAdapter.d.ts.map +1 -0
  37. package/dist/languages/LanguageAdapter.js +3 -0
  38. package/dist/languages/LanguageAdapter.js.map +1 -0
  39. package/dist/languages/PythonAdapter.d.ts +10 -0
  40. package/dist/languages/PythonAdapter.d.ts.map +1 -0
  41. package/dist/languages/PythonAdapter.js +98 -0
  42. package/dist/languages/PythonAdapter.js.map +1 -0
  43. package/dist/languages/TypeScriptAdapter.d.ts +17 -0
  44. package/dist/languages/TypeScriptAdapter.d.ts.map +1 -0
  45. package/dist/languages/TypeScriptAdapter.js +321 -0
  46. package/dist/languages/TypeScriptAdapter.js.map +1 -0
  47. package/dist/languages/registry.d.ts +4 -0
  48. package/dist/languages/registry.d.ts.map +1 -0
  49. package/dist/languages/registry.js +86 -0
  50. package/dist/languages/registry.js.map +1 -0
  51. package/dist/mapper.d.ts +49 -0
  52. package/dist/mapper.d.ts.map +1 -0
  53. package/dist/mapper.js +386 -0
  54. package/dist/mapper.js.map +1 -0
  55. package/dist/ranker-enhanced.d.ts +37 -0
  56. package/dist/ranker-enhanced.d.ts.map +1 -0
  57. package/dist/ranker-enhanced.js +395 -0
  58. package/dist/ranker-enhanced.js.map +1 -0
  59. package/dist/ranker.d.ts +14 -0
  60. package/dist/ranker.d.ts.map +1 -0
  61. package/dist/ranker.js +105 -0
  62. package/dist/ranker.js.map +1 -0
  63. package/dist/rocket-chat-scope.d.ts +7 -0
  64. package/dist/rocket-chat-scope.d.ts.map +1 -0
  65. package/dist/rocket-chat-scope.js +95 -0
  66. package/dist/rocket-chat-scope.js.map +1 -0
  67. package/dist/scanner.d.ts +16 -0
  68. package/dist/scanner.d.ts.map +1 -0
  69. package/dist/scanner.js +228 -0
  70. package/dist/scanner.js.map +1 -0
  71. package/dist/skeletonizer.d.ts +5 -0
  72. package/dist/skeletonizer.d.ts.map +1 -0
  73. package/dist/skeletonizer.js +52 -0
  74. package/dist/skeletonizer.js.map +1 -0
  75. package/dist/tokenizer.d.ts +9 -0
  76. package/dist/tokenizer.d.ts.map +1 -0
  77. package/dist/tokenizer.js +21 -0
  78. package/dist/tokenizer.js.map +1 -0
  79. package/dist/types.d.ts +68 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +6 -0
  82. package/dist/types.js.map +1 -0
  83. package/gemini-extension/GEMINI.md +26 -0
  84. package/gemini-extension/gemini-extension.json +12 -0
  85. package/gemini-extension/package.json +14 -0
  86. package/gemini-extension/src/server.ts +63 -0
  87. package/gemini-extension/tsconfig.json +14 -0
  88. package/jest.config.js +5 -0
  89. package/package.json +46 -0
  90. package/src/agent-cli.ts +383 -0
  91. package/src/agent.ts +344 -0
  92. package/src/benchmark.ts +389 -0
  93. package/src/cache.ts +317 -0
  94. package/src/cli.ts +223 -0
  95. package/src/config.ts +22 -0
  96. package/src/index.ts +215 -0
  97. package/src/languages/GenericAdapter.ts +44 -0
  98. package/src/languages/LanguageAdapter.ts +14 -0
  99. package/src/languages/PythonAdapter.ts +74 -0
  100. package/src/languages/TypeScriptAdapter.ts +338 -0
  101. package/src/languages/registry.ts +49 -0
  102. package/src/mapper.ts +448 -0
  103. package/src/ranker-enhanced.ts +460 -0
  104. package/src/ranker.ts +92 -0
  105. package/src/scanner.ts +201 -0
  106. package/src/skeletonizer.ts +16 -0
  107. package/src/tokenizer.ts +20 -0
  108. package/src/types.ts +71 -0
  109. package/tests/agent.tools.test.ts +81 -0
  110. package/tests/benchmark.test.ts +31 -0
  111. package/tests/fixtures/sample.py +17 -0
  112. package/tests/fixtures/sample.ts +13 -0
  113. package/tests/fixtures/src/api/routes.ts +1 -0
  114. package/tests/fixtures/src/auth/permission.ts +3 -0
  115. package/tests/ranker-enhanced.test.ts +68 -0
  116. package/tests/ranker.test.ts +75 -0
  117. package/tests/scanner.scope.test.ts +41 -0
  118. package/tests/setup-fixtures.js +29 -0
  119. package/tests/skeletonizer.test.ts +142 -0
  120. package/tsconfig.json +21 -0
  121. package/uca-landing/index.html +17 -0
  122. package/uca-landing/package.json +23 -0
  123. package/uca-landing/postcss.config.js +6 -0
  124. package/uca-landing/src/App.jsx +43 -0
  125. package/uca-landing/src/components/AgentMode.jsx +45 -0
  126. package/uca-landing/src/components/CliReference.jsx +49 -0
  127. package/uca-landing/src/components/Features.jsx +63 -0
  128. package/uca-landing/src/components/Footer.jsx +35 -0
  129. package/uca-landing/src/components/Hero.jsx +124 -0
  130. package/uca-landing/src/components/HowItWorks.jsx +60 -0
  131. package/uca-landing/src/components/Install.jsx +90 -0
  132. package/uca-landing/src/components/LanguageSupport.jsx +63 -0
  133. package/uca-landing/src/components/Navbar.jsx +86 -0
  134. package/uca-landing/src/components/Problem.jsx +51 -0
  135. package/uca-landing/src/components/Reveal.jsx +40 -0
  136. package/uca-landing/src/components/WorksWith.jsx +59 -0
  137. package/uca-landing/src/hooks/useScrollNav.js +13 -0
  138. package/uca-landing/src/hooks/useTypewriter.js +41 -0
  139. package/uca-landing/src/index.css +13 -0
  140. package/uca-landing/src/main.jsx +10 -0
  141. package/uca-landing/tailwind.config.js +68 -0
  142. package/uca-landing/vite.config.js +6 -0
package/src/scanner.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * File scanner for universal codebases.
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+
8
+ export const DEFAULT_EXCLUDES = [
9
+ '**/node_modules/**',
10
+ '**/.git/**',
11
+ '**/dist/**',
12
+ '**/build/**',
13
+ '**/coverage/**',
14
+ '**/__pycache__/**',
15
+ '**/*.min.js',
16
+ '**/*.bundle.js',
17
+ '**/vendor/**',
18
+ '**/.next/**',
19
+ '**/.nuxt/**',
20
+ '**/target/**',
21
+ '**/*.pyc',
22
+ '**/.cache/**',
23
+ ];
24
+
25
+ const DEFAULT_IGNORED_DIRS = new Set([
26
+ 'node_modules',
27
+ '.git',
28
+ 'dist',
29
+ 'build',
30
+ 'coverage',
31
+ '__pycache__',
32
+ 'vendor',
33
+ '.next',
34
+ '.nuxt',
35
+ 'target',
36
+ '.cache',
37
+ '.idea',
38
+ '.vscode',
39
+ ]);
40
+
41
+ const SRC_CANDIDATES = ['src', 'lib', 'packages', 'apps', 'modules'];
42
+
43
+ export interface ScanOptions {
44
+ extensions?: string[];
45
+ exclude?: string[];
46
+ moduleKeys?: string[];
47
+ moduleDefinitions?: Record<string, string[]>;
48
+ }
49
+
50
+ export function discoverModules(root: string): string[] {
51
+ const srcCandidates = ['src', 'lib', 'packages', 'apps', 'modules'];
52
+
53
+ for (const candidate of srcCandidates) {
54
+ const candidatePath = path.join(root, candidate);
55
+ if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory()) {
56
+ const subdirs = fs.readdirSync(candidatePath)
57
+ .filter((name) => {
58
+ const fullPath = path.join(candidatePath, name);
59
+ return fs.statSync(fullPath).isDirectory() && !name.startsWith('.');
60
+ });
61
+ if (subdirs.length > 0) return subdirs;
62
+ }
63
+ }
64
+
65
+ const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.cache', '__pycache__', 'vendor']);
66
+ return fs.readdirSync(root)
67
+ .filter((name) => {
68
+ const fullPath = path.join(root, name);
69
+ return fs.statSync(fullPath).isDirectory() && !name.startsWith('.') && !ignored.has(name);
70
+ });
71
+ }
72
+
73
+ export function scanFiles(rootDir: string, options: ScanOptions = {}): string[] {
74
+ const results: string[] = [];
75
+ const excludes = options.exclude ?? DEFAULT_EXCLUDES;
76
+ const extensions = options.extensions?.map((ext) => ext.toLowerCase());
77
+ const scanRoots = resolveScanRoots(rootDir, options);
78
+
79
+ const scan = (dir: string): void => {
80
+ let entries: fs.Dirent[];
81
+ try {
82
+ entries = fs.readdirSync(dir, { withFileTypes: true });
83
+ } catch {
84
+ return;
85
+ }
86
+
87
+ for (const entry of entries) {
88
+ const fullPath = path.join(dir, entry.name);
89
+ if (shouldExclude(fullPath, rootDir, excludes)) {
90
+ continue;
91
+ }
92
+
93
+ if (entry.isDirectory()) {
94
+ if (!DEFAULT_IGNORED_DIRS.has(entry.name)) {
95
+ scan(fullPath);
96
+ }
97
+ } else if (entry.isFile()) {
98
+ if (!extensions || extensions.length === 0) {
99
+ results.push(fullPath);
100
+ continue;
101
+ }
102
+ const ext = path.extname(entry.name).toLowerCase();
103
+ if (extensions.includes(ext)) {
104
+ results.push(fullPath);
105
+ }
106
+ }
107
+ }
108
+ };
109
+
110
+ for (const scanRoot of scanRoots) {
111
+ if (fs.existsSync(scanRoot) && fs.statSync(scanRoot).isDirectory()) {
112
+ scan(scanRoot);
113
+ }
114
+ }
115
+
116
+ return Array.from(new Set(results));
117
+ }
118
+
119
+ export function scanTypeScriptFiles(rootDir: string): string[] {
120
+ return scanFiles(rootDir, { extensions: ['.ts', '.tsx'] });
121
+ }
122
+
123
+ export function scanRocketChatFiles(rootDir: string, moduleKeys?: string[]): string[] {
124
+ return scanFiles(rootDir, {
125
+ extensions: ['.ts', '.tsx'],
126
+ moduleKeys,
127
+ });
128
+ }
129
+
130
+ export function readFileContent(filePath: string): string | null {
131
+ try {
132
+ return fs.readFileSync(filePath, 'utf-8');
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ function resolveScanRoots(rootDir: string, options: ScanOptions): string[] {
139
+ const moduleKeys = options.moduleKeys;
140
+ const moduleDefinitions = options.moduleDefinitions;
141
+ if (!moduleKeys || moduleKeys.length === 0) {
142
+ return [rootDir];
143
+ }
144
+
145
+ const resolved: string[] = [];
146
+ for (const key of moduleKeys) {
147
+ if (moduleDefinitions?.[key]) {
148
+ for (const rel of moduleDefinitions[key]) {
149
+ const full = path.resolve(rootDir, rel);
150
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
151
+ resolved.push(full);
152
+ }
153
+ }
154
+ continue;
155
+ }
156
+
157
+ const direct = path.join(rootDir, key);
158
+ if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
159
+ resolved.push(direct);
160
+ continue;
161
+ }
162
+
163
+ for (const base of SRC_CANDIDATES) {
164
+ const candidate = path.join(rootDir, base, key);
165
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
166
+ resolved.push(candidate);
167
+ }
168
+ }
169
+ }
170
+
171
+ if (resolved.length === 0 && moduleKeys.length > 0) {
172
+ throw new Error(`No matching modules found for keys: ${moduleKeys.join(', ')}`);
173
+ }
174
+
175
+ return resolved;
176
+ }
177
+
178
+ function shouldExclude(fullPath: string, rootDir: string, excludes: string[]): boolean {
179
+ const rel = path.relative(rootDir, fullPath).split(path.sep).join('/');
180
+ const hasSegment = (segment: string): boolean =>
181
+ rel === segment || rel.startsWith(`${segment}/`) || rel.includes(`/${segment}/`);
182
+
183
+ for (const pattern of excludes) {
184
+ if (pattern.includes('node_modules') && hasSegment('node_modules')) return true;
185
+ if (pattern.includes('.git') && hasSegment('.git')) return true;
186
+ if (pattern.includes('/dist/') && hasSegment('dist')) return true;
187
+ if (pattern.includes('/build/') && hasSegment('build')) return true;
188
+ if (pattern.includes('/coverage/') && hasSegment('coverage')) return true;
189
+ if (pattern.includes('__pycache__') && hasSegment('__pycache__')) return true;
190
+ if (pattern.includes('/vendor/') && hasSegment('vendor')) return true;
191
+ if (pattern.includes('/.next/') && hasSegment('.next')) return true;
192
+ if (pattern.includes('/.nuxt/') && hasSegment('.nuxt')) return true;
193
+ if (pattern.includes('/target/') && hasSegment('target')) return true;
194
+ if (pattern.includes('/.cache/') && hasSegment('.cache')) return true;
195
+ if (pattern.endsWith('*.min.js') && rel.endsWith('.min.js')) return true;
196
+ if (pattern.endsWith('*.bundle.js') && rel.endsWith('.bundle.js')) return true;
197
+ if (pattern.endsWith('*.pyc') && rel.endsWith('.pyc')) return true;
198
+ }
199
+
200
+ return false;
201
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Language-aware skeleton generation.
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import { getAdapter } from './languages/registry';
7
+
8
+ export function buildSkeletonForFile(filePath: string): string {
9
+ try {
10
+ const content = fs.readFileSync(filePath, 'utf-8');
11
+ const adapter = getAdapter(filePath);
12
+ return adapter.generateSkeleton(content, filePath);
13
+ } catch (error) {
14
+ return `/* Failed to parse file: ${error instanceof Error ? error.message : 'Unknown error'} */`;
15
+ }
16
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Simple tokenizer for counting approximate tokens in code
3
+ */
4
+
5
+ /**
6
+ * Count approximate tokens in a string by splitting on whitespace and punctuation
7
+ * This is a simple heuristic for estimating token count
8
+ */
9
+ export function countTokens(text: string): number {
10
+ if (!text || text.trim().length === 0) {
11
+ return 0;
12
+ }
13
+
14
+ // Split on whitespace, punctuation, and common code delimiters
15
+ const tokens = text
16
+ .split(/[\s\n\r\t.,;:!?(){}[\]<>'"/\\|`~@#$%^&*+=-]+/)
17
+ .filter(token => token.length > 0);
18
+
19
+ return tokens.length;
20
+ }
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared type definitions for skannr
3
+ */
4
+
5
+ /**
6
+ * Options for analyzing a project
7
+ */
8
+ export interface AnalyzeOptions {
9
+ /** Root directory of the project */
10
+ root: string;
11
+ /** Natural language question about the codebase */
12
+ question: string;
13
+ /** Number of top files to return */
14
+ limit?: number;
15
+ /** Generate symbol mapping for on-demand retrieval */
16
+ generateMapping?: boolean;
17
+ /** Output path for mapping file (default: <root>/code-analyzer.mapping.json) */
18
+ mappingOutputPath?: string;
19
+ /** Use enhanced ranking algorithm with dependency analysis */
20
+ enhancedRanking?: boolean;
21
+ /** Optional filter for module keys (e.g. ['auth', 'api']) */
22
+ moduleKeys?: string[];
23
+ /** Optional module definitions from config (module -> directory list) */
24
+ moduleDefinitions?: Record<string, string[]>;
25
+ /** Optional exclusion globs */
26
+ exclude?: string[];
27
+ /** Optional extension allow-list */
28
+ extensions?: string[];
29
+ /** Language strategy */
30
+ lang?: 'typescript' | 'javascript' | 'python' | 'auto';
31
+ }
32
+
33
+ /**
34
+ * Ranked file with relevance score
35
+ */
36
+ export interface RankedFile {
37
+ /** Absolute path to the file */
38
+ path: string;
39
+ /** Relevance score (0-1) */
40
+ score: number;
41
+ }
42
+
43
+ /**
44
+ * Analysis result for a single file
45
+ */
46
+ export interface FileAnalysis {
47
+ /** Relative path to the file */
48
+ path: string;
49
+ /** Relevance score (0-1) */
50
+ score: number;
51
+ /** Generated skeleton code */
52
+ skeleton: string;
53
+ /** Approximate token count of original file */
54
+ originalTokenCount: number;
55
+ /** Approximate token count of skeleton */
56
+ skeletonTokenCount: number;
57
+ }
58
+
59
+ /**
60
+ * Complete analysis result
61
+ */
62
+ export interface AnalysisResult {
63
+ /** The original question */
64
+ question: string;
65
+ /** The project root directory */
66
+ root: string;
67
+ /** Number of files requested */
68
+ limit: number;
69
+ /** Analyzed files with skeletons */
70
+ files: FileAnalysis[];
71
+ }
@@ -0,0 +1,81 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { CodeAnalysisAgent } from '../src/agent';
5
+ import { buildSkeletonWithMapping, saveMappingToFile, type SymbolMapping } from '../src/mapper';
6
+
7
+ describe('CodeAnalysisAgent local tool wrappers', () => {
8
+ let tempDir: string;
9
+ let sampleFile: string;
10
+ let mappingPath: string;
11
+ let agent: CodeAnalysisAgent;
12
+
13
+ beforeAll(() => {
14
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'uca-agent-test-'));
15
+ sampleFile = path.join(tempDir, 'sample.ts');
16
+ mappingPath = path.join(tempDir, 'code-analyzer.mapping.json');
17
+
18
+ fs.writeFileSync(
19
+ sampleFile,
20
+ [
21
+ 'export class AuthService {',
22
+ ' login(user: string): boolean {',
23
+ ' return user.length > 0;',
24
+ ' }',
25
+ '}',
26
+ '',
27
+ 'export function sendMessage(room: string, text: string): string {',
28
+ ' return `${room}:${text}`;',
29
+ '}',
30
+ ].join('\n'),
31
+ 'utf-8'
32
+ );
33
+
34
+ const mappingResult = buildSkeletonWithMapping(sampleFile, tempDir);
35
+ const mapping: SymbolMapping = {
36
+ generatedAt: new Date().toISOString(),
37
+ rootPath: tempDir,
38
+ files: {
39
+ 'sample.ts': {
40
+ originalPath: sampleFile,
41
+ symbols: mappingResult.symbols,
42
+ },
43
+ },
44
+ };
45
+
46
+ saveMappingToFile(mapping, mappingPath);
47
+
48
+ agent = new CodeAnalysisAgent('test-api-key');
49
+ agent.loadMapping(mappingPath);
50
+ });
51
+
52
+ afterAll(() => {
53
+ if (fs.existsSync(tempDir)) {
54
+ fs.rmSync(tempDir, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ it('searchSymbols should find matching symbols', () => {
59
+ const result = agent.searchSymbols('login');
60
+ expect(result.error).toBeUndefined();
61
+ expect(result.count).toBeGreaterThan(0);
62
+
63
+ const found = result.results.some((item: any) => item.symbolId === 'AuthService.login');
64
+ expect(found).toBe(true);
65
+ });
66
+
67
+ it('getSymbolDetails should return implementation with location', () => {
68
+ const result = agent.getSymbolDetails('sendMessage');
69
+ expect(result.error).toBeUndefined();
70
+ expect(result.content).toContain('return `${room}:${text}`;');
71
+ expect(result.location.file).toBe('sample.ts');
72
+ });
73
+
74
+ it('analyzeFileDependencies should return symbol list for file', () => {
75
+ const result = agent.analyzeFileDependencies('sample.ts');
76
+ expect(result.error).toBeUndefined();
77
+ expect(result.file).toBe('sample.ts');
78
+ expect(Array.isArray(result.symbols)).toBe(true);
79
+ expect(result.symbols.length).toBeGreaterThan(0);
80
+ });
81
+ });
@@ -0,0 +1,31 @@
1
+ import { benchmarkProject } from '../src/benchmark';
2
+ import * as path from 'path';
3
+
4
+ describe('Benchmark Tests', () => {
5
+ it('BENCHMARK_SCENARIOS module keys are strings', () => {
6
+ const { BENCHMARK_SCENARIOS } = require('../src/benchmark');
7
+ BENCHMARK_SCENARIOS.forEach((scenario: { moduleKey: string }) => {
8
+ expect(typeof scenario.moduleKey).toBe('string');
9
+ expect(scenario.moduleKey.length).toBeGreaterThan(0);
10
+ });
11
+ });
12
+
13
+ it('Scenario path resolution is deterministic and scoped', async () => {
14
+ const fixtureRoot = path.join(__dirname, 'fixtures');
15
+ const moduleKey = 'src';
16
+
17
+ const benchmarkResult = await benchmarkProject(fixtureRoot, moduleKey, 'Test Project', 'authentication flow');
18
+
19
+ const expectedPath = path.join(fixtureRoot, 'src');
20
+ expect(benchmarkResult.projectRoot).toBe(expectedPath);
21
+
22
+ expect(benchmarkResult.top1KeywordCoverage).toBeGreaterThanOrEqual(0);
23
+ expect(benchmarkResult.top1KeywordCoverage).toBeLessThanOrEqual(1);
24
+ expect(benchmarkResult.avgTopKKeywordCoverage).toBeGreaterThanOrEqual(0);
25
+ expect(benchmarkResult.avgTopKKeywordCoverage).toBeLessThanOrEqual(1);
26
+ expect(benchmarkResult.pathIntentMatchRate).toBeGreaterThanOrEqual(0);
27
+ expect(benchmarkResult.pathIntentMatchRate).toBeLessThanOrEqual(1);
28
+ expect(benchmarkResult.directoryDiversity).toBeGreaterThanOrEqual(0);
29
+ expect(benchmarkResult.directoryDiversity).toBeLessThanOrEqual(1);
30
+ });
31
+ });
@@ -0,0 +1,17 @@
1
+ import os
2
+
3
+
4
+ class UserStore:
5
+ def __init__(self, root: str) -> None:
6
+ self.root = root
7
+
8
+ def path_for(self, user_id: str) -> str:
9
+ return os.path.join(self.root, user_id)
10
+
11
+
12
+ def normalize_username(value: str) -> str:
13
+ return value.strip().lower()
14
+
15
+
16
+ def can_login(enabled: bool, attempts: int) -> bool:
17
+ return enabled and attempts < 5
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'fs';
2
+
3
+ export class SampleService {
4
+ constructor(private readonly source: string) {}
5
+
6
+ load(): string {
7
+ return readFileSync(this.source, 'utf-8');
8
+ }
9
+ }
10
+
11
+ export function formatMessage(name: string): string {
12
+ return `hello ${name}`;
13
+ }
@@ -0,0 +1 @@
1
+ export const routes = ['/health', '/users'];
@@ -0,0 +1,3 @@
1
+ export function hasPermission(role: string, action: string): boolean {
2
+ return role === 'admin' || action === 'read';
3
+ }
@@ -0,0 +1,68 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { rankFilesEnhanced } from '../src/ranker-enhanced';
5
+
6
+ describe('rankFilesEnhanced hybrid retrieval', () => {
7
+ let tempDir: string;
8
+
9
+ beforeEach(() => {
10
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ranker-enhanced-'));
11
+ });
12
+
13
+ afterEach(() => {
14
+ if (fs.existsSync(tempDir)) {
15
+ fs.rmSync(tempDir, { recursive: true, force: true });
16
+ }
17
+ });
18
+
19
+ it('prioritizes files aligned with auth intent', () => {
20
+ const authFile = path.join(tempDir, 'auth-permissions.ts');
21
+ const genericFile = path.join(tempDir, 'utility.ts');
22
+
23
+ fs.writeFileSync(
24
+ authFile,
25
+ [
26
+ 'export class AuthorizationService {',
27
+ ' checkPermission(user: string, action: string): boolean {',
28
+ ' return !!user && !!action;',
29
+ ' }',
30
+ '}',
31
+ ].join('\n'),
32
+ 'utf-8'
33
+ );
34
+
35
+ fs.writeFileSync(
36
+ genericFile,
37
+ [
38
+ 'export function sum(a: number, b: number): number {',
39
+ ' return a + b;',
40
+ '}',
41
+ ].join('\n'),
42
+ 'utf-8'
43
+ );
44
+
45
+ const ranked = rankFilesEnhanced(
46
+ [authFile, genericFile],
47
+ 'how are permissions validated for users?',
48
+ 2,
49
+ tempDir
50
+ );
51
+
52
+ expect(ranked.length).toBe(2);
53
+ expect(path.basename(ranked[0].path)).toBe('auth-permissions.ts');
54
+ });
55
+
56
+ it('returns deterministic ordering for equal scores', () => {
57
+ const aFile = path.join(tempDir, 'a.ts');
58
+ const bFile = path.join(tempDir, 'b.ts');
59
+
60
+ fs.writeFileSync(aFile, 'export const a = 1;', 'utf-8');
61
+ fs.writeFileSync(bFile, 'export const b = 2;', 'utf-8');
62
+
63
+ const ranked = rankFilesEnhanced([bFile, aFile], 'zzzz-unmatched-term', 2, tempDir);
64
+
65
+ expect(ranked.length).toBe(2);
66
+ expect(ranked[0].path <= ranked[1].path).toBe(true);
67
+ });
68
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tests for file ranking functionality
3
+ */
4
+
5
+ import { calculateRelevanceScore, rankFiles } from '../src/ranker';
6
+
7
+ describe('calculateRelevanceScore', () => {
8
+ it('should return 0 for null content', () => {
9
+ const score = calculateRelevanceScore('/path/to/file.ts', null, 'test question');
10
+ expect(score).toBe(0);
11
+ });
12
+
13
+ it('should return higher score for filename matches', () => {
14
+ const filePath = '/src/authentication/auth-service.ts';
15
+ const content = 'export class Service {}';
16
+ const score = calculateRelevanceScore(filePath, content, 'authentication');
17
+ expect(score).toBeGreaterThanOrEqual(0.3);
18
+ });
19
+
20
+ it('should return higher score for content matches', () => {
21
+ const filePath = '/src/service.ts';
22
+ const content = `
23
+ export class AuthService {
24
+ async login() { }
25
+ async logout() { }
26
+ authenticate() { }
27
+ }
28
+ `;
29
+ const score = calculateRelevanceScore(filePath, content, 'authentication login');
30
+ expect(score).toBeGreaterThan(0);
31
+ });
32
+
33
+ it('should return neutral score for empty question', () => {
34
+ const score = calculateRelevanceScore('/path/to/file.ts', 'content', '');
35
+ expect(score).toBe(0.5);
36
+ });
37
+
38
+ it('should handle multiple word questions', () => {
39
+ const filePath = '/src/user/profile.ts';
40
+ const content = 'export class UserProfile {}';
41
+ const score = calculateRelevanceScore(filePath, content, 'user profile settings');
42
+ expect(score).toBeGreaterThan(0);
43
+ });
44
+ });
45
+
46
+ describe('rankFiles', () => {
47
+ it('should return empty array for no files', () => {
48
+ const ranked = rankFiles([], 'test question', 10);
49
+ expect(ranked).toEqual([]);
50
+ });
51
+
52
+ it('should limit results to requested number', () => {
53
+ // Create mock files - note: rankFiles reads from filesystem
54
+ // For a real test, we'd need actual files, but this tests the logic
55
+ const files = [
56
+ '/test/auth.ts',
57
+ '/test/user.ts',
58
+ '/test/service.ts',
59
+ ];
60
+
61
+ // Mock would be needed here for a full test
62
+ // This is a placeholder to show the structure
63
+ const ranked = rankFiles(files, 'test', 2);
64
+ expect(ranked.length).toBeLessThanOrEqual(2);
65
+ });
66
+
67
+ it('should sort by score descending', () => {
68
+ const files = ['/test/file1.ts', '/test/file2.ts'];
69
+ const ranked = rankFiles(files, 'test', 10);
70
+
71
+ for (let i = 1; i < ranked.length; i++) {
72
+ expect(ranked[i - 1].score).toBeGreaterThanOrEqual(ranked[i].score);
73
+ }
74
+ });
75
+ });
@@ -0,0 +1,41 @@
1
+ import * as path from 'path';
2
+ import { discoverModules, scanFiles } from '../src/scanner';
3
+
4
+ const rootDir = path.join(__dirname, 'fixtures');
5
+
6
+ describe('generic scanner', () => {
7
+ it('discovers modules from common source folders', () => {
8
+ const modules = discoverModules(rootDir);
9
+ expect(modules).toContain('auth');
10
+ expect(modules).toContain('api');
11
+ });
12
+
13
+ it('scans only requested module keys', () => {
14
+ const files = scanFiles(rootDir, {
15
+ moduleKeys: ['auth'],
16
+ moduleDefinitions: {
17
+ auth: ['src/auth'],
18
+ },
19
+ extensions: ['.ts'],
20
+ });
21
+ expect(files.length).toBeGreaterThan(0);
22
+ files.forEach((file) => {
23
+ expect(file.startsWith(path.join(rootDir, 'src/auth'))).toBe(true);
24
+ });
25
+ });
26
+
27
+ it('returns empty array on invalid root', () => {
28
+ const invalidRoot = path.join(__dirname, 'missing-fixtures');
29
+ const files = scanFiles(invalidRoot, { extensions: ['.ts'] });
30
+ expect(files).toEqual([]);
31
+ });
32
+
33
+ it('excludes common ignored folders', () => {
34
+ const files = scanFiles(rootDir, { extensions: ['.ts'] });
35
+ const excludedDir = path.join(rootDir, 'node_modules');
36
+
37
+ files.forEach((file) => {
38
+ expect(file.startsWith(excludedDir)).toBe(false);
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,29 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // The fake repo root expected by the tests
5
+ const root = path.join(__dirname, 'fixtures');
6
+
7
+ const dirs = [
8
+ 'src/auth',
9
+ 'src/api',
10
+ 'node_modules',
11
+ ];
12
+
13
+ const files = [
14
+ 'src/auth/permission.ts',
15
+ 'src/api/routes.ts',
16
+ 'node_modules/ignored.ts',
17
+ ];
18
+
19
+ console.log(`Creating test fixtures in: ${root}`);
20
+
21
+ dirs.forEach((dir) => {
22
+ fs.mkdirSync(path.join(root, dir), { recursive: true });
23
+ });
24
+
25
+ files.forEach((file) => {
26
+ fs.writeFileSync(path.join(root, file), '// test file content');
27
+ });
28
+
29
+ console.log('✅ Fixtures created successfully.');