skannr 0.1.6 → 0.2.0

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 (58) hide show
  1. package/dist/languages/TreeSitterAdapter.d.ts +30 -0
  2. package/dist/languages/TreeSitterAdapter.d.ts.map +1 -0
  3. package/dist/languages/TreeSitterAdapter.js +466 -0
  4. package/dist/languages/TreeSitterAdapter.js.map +1 -0
  5. package/dist/languages/grammars/tree-sitter-go.wasm +0 -0
  6. package/dist/languages/grammars/tree-sitter-java.wasm +0 -0
  7. package/dist/languages/grammars/tree-sitter-python.wasm +0 -0
  8. package/dist/languages/grammars/tree-sitter-rust.wasm +0 -0
  9. package/dist/languages/lang-config.d.ts +38 -0
  10. package/dist/languages/lang-config.d.ts.map +1 -0
  11. package/dist/languages/lang-config.js +85 -0
  12. package/dist/languages/lang-config.js.map +1 -0
  13. package/dist/languages/registry.d.ts +6 -0
  14. package/dist/languages/registry.d.ts.map +1 -1
  15. package/dist/languages/registry.js +18 -4
  16. package/dist/languages/registry.js.map +1 -1
  17. package/package.json +4 -7
  18. package/src/languages/TreeSitterAdapter.ts +431 -0
  19. package/src/languages/lang-config.ts +113 -0
  20. package/src/languages/registry.ts +65 -49
  21. package/src/languages/PythonAdapter.ts +0 -302
  22. package/tests/agent.tools.test.ts +0 -81
  23. package/tests/benchmark.test.ts +0 -31
  24. package/tests/blast-radius.test.ts +0 -290
  25. package/tests/fixtures/sample.py +0 -17
  26. package/tests/fixtures/sample.ts +0 -13
  27. package/tests/fixtures/src/api/routes.ts +0 -1
  28. package/tests/fixtures/src/auth/permission.ts +0 -3
  29. package/tests/python-adapter.test.ts +0 -31
  30. package/tests/ranker-enhanced.test.ts +0 -70
  31. package/tests/ranker.test.ts +0 -79
  32. package/tests/scanner.scope.test.ts +0 -67
  33. package/tests/setup-fixtures.js +0 -29
  34. package/tests/skeletonizer.test.ts +0 -149
  35. package/uca-landing/index.html +0 -17
  36. package/uca-landing/package.json +0 -23
  37. package/uca-landing/postcss.config.js +0 -6
  38. package/uca-landing/src/App.jsx +0 -45
  39. package/uca-landing/src/components/AgentMode.jsx +0 -45
  40. package/uca-landing/src/components/CliReference.jsx +0 -89
  41. package/uca-landing/src/components/Features.jsx +0 -88
  42. package/uca-landing/src/components/Footer.jsx +0 -35
  43. package/uca-landing/src/components/Hero.jsx +0 -125
  44. package/uca-landing/src/components/HowItWorks.jsx +0 -65
  45. package/uca-landing/src/components/Install.jsx +0 -103
  46. package/uca-landing/src/components/LanguageSupport.jsx +0 -63
  47. package/uca-landing/src/components/Navbar.jsx +0 -86
  48. package/uca-landing/src/components/Problem.jsx +0 -51
  49. package/uca-landing/src/components/Reveal.jsx +0 -40
  50. package/uca-landing/src/components/TokenSavings.jsx +0 -126
  51. package/uca-landing/src/components/WorksWith.jsx +0 -59
  52. package/uca-landing/src/hooks/useScrollNav.js +0 -13
  53. package/uca-landing/src/hooks/useTypewriter.js +0 -41
  54. package/uca-landing/src/index.css +0 -13
  55. package/uca-landing/src/main.jsx +0 -10
  56. package/uca-landing/tailwind.config.js +0 -68
  57. package/uca-landing/vercel.json +0 -4
  58. package/uca-landing/vite.config.js +0 -6
@@ -1,49 +1,65 @@
1
- import { LanguageAdapter } from './LanguageAdapter';
2
- import { TypeScriptAdapter } from './TypeScriptAdapter';
3
- import { PythonAdapter } from './PythonAdapter';
4
- import { GenericAdapter } from './GenericAdapter';
5
- import * as path from 'path';
6
-
7
- const adapters: LanguageAdapter[] = [
8
- new TypeScriptAdapter(),
9
- new PythonAdapter(),
10
- ];
11
-
12
- const generic = new GenericAdapter();
13
-
14
- export function getAdapter(filePath: string): LanguageAdapter {
15
- const ext = path.extname(filePath).toLowerCase();
16
- return adapters.find((a) => a.extensions.includes(ext)) ?? generic;
17
- }
18
-
19
- export function detectRepoLanguages(root: string): string[] {
20
- const fs = require('fs');
21
- const extCount: Record<string, number> = {};
22
- walkAndCount(root, extCount, 0, 3);
23
- return Object.entries(extCount)
24
- .sort((a, b) => b[1] - a[1])
25
- .slice(0, 3)
26
- .map(([ext]) => ext);
27
- }
28
-
29
- function walkAndCount(dir: string, counts: Record<string, number>, depth: number, maxDepth: number): void {
30
- if (depth > maxDepth) return;
31
- const fs = require('fs');
32
- const path = require('path');
33
- const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '__pycache__']);
34
- try {
35
- for (const entry of fs.readdirSync(dir)) {
36
- if (ignored.has(entry)) continue;
37
- const full = path.join(dir, entry);
38
- const stat = fs.statSync(full);
39
- if (stat.isDirectory()) {
40
- walkAndCount(full, counts, depth + 1, maxDepth);
41
- } else {
42
- const ext = path.extname(entry).toLowerCase();
43
- if (ext) counts[ext] = (counts[ext] ?? 0) + 1;
44
- }
45
- }
46
- } catch {
47
- // Ignore unreadable directories.
48
- }
49
- }
1
+ /**
2
+ * Language adapter registry.
3
+ * Routes files to the appropriate parser based on extension.
4
+ */
5
+
6
+ import { LanguageAdapter } from './LanguageAdapter';
7
+ import { TypeScriptAdapter } from './TypeScriptAdapter';
8
+ import { TreeSitterAdapter } from './TreeSitterAdapter';
9
+ import { GenericAdapter } from './GenericAdapter';
10
+ import { LANG_CONFIGS } from './lang-config';
11
+ import * as path from 'path';
12
+
13
+ const tsAdapter = new TypeScriptAdapter();
14
+ const treeSitterAdapter = new TreeSitterAdapter(Object.values(LANG_CONFIGS));
15
+ const generic = new GenericAdapter();
16
+
17
+ // Initialize tree-sitter WASM in background (non-blocking).
18
+ treeSitterAdapter.initializeSync();
19
+
20
+ const adapters: LanguageAdapter[] = [
21
+ tsAdapter,
22
+ treeSitterAdapter,
23
+ ];
24
+
25
+ export function getAdapter(filePath: string): LanguageAdapter {
26
+ const ext = path.extname(filePath).toLowerCase();
27
+ return adapters.find((a) => a.extensions.includes(ext)) ?? generic;
28
+ }
29
+
30
+ /** Await full initialization of WASM-based adapters. */
31
+ export async function initializeAdapters(): Promise<void> {
32
+ await treeSitterAdapter.initialize();
33
+ }
34
+
35
+ export function detectRepoLanguages(root: string): string[] {
36
+ const fs = require('fs');
37
+ const extCount: Record<string, number> = {};
38
+ walkAndCount(root, extCount, 0, 3);
39
+ return Object.entries(extCount)
40
+ .sort((a, b) => b[1] - a[1])
41
+ .slice(0, 3)
42
+ .map(([ext]) => ext);
43
+ }
44
+
45
+ function walkAndCount(dir: string, counts: Record<string, number>, depth: number, maxDepth: number): void {
46
+ if (depth > maxDepth) return;
47
+ const fs = require('fs');
48
+ const path = require('path');
49
+ const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '__pycache__']);
50
+ try {
51
+ for (const entry of fs.readdirSync(dir)) {
52
+ if (ignored.has(entry)) continue;
53
+ const full = path.join(dir, entry);
54
+ const stat = fs.statSync(full);
55
+ if (stat.isDirectory()) {
56
+ walkAndCount(full, counts, depth + 1, maxDepth);
57
+ } else {
58
+ const ext = path.extname(entry).toLowerCase();
59
+ if (ext) counts[ext] = (counts[ext] ?? 0) + 1;
60
+ }
61
+ }
62
+ } catch {
63
+ // Ignore unreadable directories.
64
+ }
65
+ }
@@ -1,302 +0,0 @@
1
- import * as path from 'path';
2
- import { LanguageAdapter, SkeletonResult, Symbol } from './LanguageAdapter';
3
-
4
- let Parser: any;
5
- let pythonLanguage: any;
6
- let treeSitterAvailable = true;
7
-
8
- try {
9
- Parser = require('tree-sitter');
10
- } catch {
11
- treeSitterAvailable = false;
12
- }
13
-
14
- type SyntaxNode = any;
15
-
16
- function ensureParserLoaded(): boolean {
17
- if (!treeSitterAvailable) return false;
18
- if (!pythonLanguage) {
19
- try {
20
- // eslint-disable-next-line @typescript-eslint/no-require-imports
21
- pythonLanguage = require('tree-sitter-python');
22
- } catch {
23
- treeSitterAvailable = false;
24
- return false;
25
- }
26
- }
27
- return true;
28
- }
29
-
30
- function getParser(): any | null {
31
- if (!ensureParserLoaded()) return null;
32
- const parser = new Parser();
33
- parser.setLanguage(pythonLanguage!);
34
- return parser;
35
- }
36
-
37
- function collapseOneLineBody(line: string): string {
38
- const idx = line.indexOf(':');
39
- if (idx === -1) {
40
- return line;
41
- }
42
- return `${line.slice(0, idx + 1)} ...`;
43
- }
44
-
45
- function baseIndent(line: string | undefined): string {
46
- if (!line) {
47
- return '';
48
- }
49
- return line.match(/^(\s*)/)?.[1] ?? '';
50
- }
51
-
52
- interface SkeletonState {
53
- lines: string[];
54
- keep: Set<number>;
55
- replace: Map<number, string>;
56
- ellipsisAfter: Set<number>;
57
- }
58
-
59
- function addRange(state: SkeletonState, start: number, end: number): void {
60
- const max = state.lines.length - 1;
61
- const lo = Math.max(0, start);
62
- const hi = Math.min(max, end);
63
- for (let i = lo; i <= hi; i++) {
64
- state.keep.add(i);
65
- }
66
- }
67
-
68
- function emitFunctionHeader(state: SkeletonState, node: SyntaxNode): void {
69
- const body = node.childForFieldName('body');
70
- const start = node.startPosition.row;
71
- if (!body) {
72
- addRange(state, start, node.endPosition.row);
73
- return;
74
- }
75
- if (body.startPosition.row > start) {
76
- addRange(state, start, body.startPosition.row - 1);
77
- state.ellipsisAfter.add(body.startPosition.row - 1);
78
- return;
79
- }
80
- state.replace.set(start, collapseOneLineBody(state.lines[start]));
81
- }
82
-
83
- function emitClassHeaderLines(state: SkeletonState, node: SyntaxNode): void {
84
- const body = node.childForFieldName('body');
85
- const start = node.startPosition.row;
86
- if (!body) {
87
- addRange(state, start, node.endPosition.row);
88
- return;
89
- }
90
- if (body.startPosition.row > start) {
91
- addRange(state, start, body.startPosition.row - 1);
92
- return;
93
- }
94
- state.replace.set(start, collapseOneLineBody(state.lines[start]));
95
- }
96
-
97
- function processDecoratedDefinition(state: SkeletonState, node: SyntaxNode): void {
98
- for (const child of node.namedChildren) {
99
- if (child.type === 'decorator') {
100
- addRange(state, child.startPosition.row, child.endPosition.row);
101
- } else {
102
- processStatement(state, child);
103
- }
104
- }
105
- }
106
-
107
- function processStatement(state: SkeletonState, node: SyntaxNode): void {
108
- switch (node.type) {
109
- case 'import_statement':
110
- case 'import_from_statement':
111
- addRange(state, node.startPosition.row, node.endPosition.row);
112
- return;
113
- case 'type_alias_statement':
114
- addRange(state, node.startPosition.row, node.endPosition.row);
115
- return;
116
- case 'decorated_definition':
117
- processDecoratedDefinition(state, node);
118
- return;
119
- case 'function_definition':
120
- emitFunctionHeader(state, node);
121
- return;
122
- case 'class_definition': {
123
- emitClassHeaderLines(state, node);
124
- const body = node.childForFieldName('body');
125
- if (body?.type !== 'block') {
126
- return;
127
- }
128
- const start = node.startPosition.row;
129
- const meaningful = body.namedChildren.filter(
130
- (c: SyntaxNode) => c.type !== 'pass_statement',
131
- );
132
- if (meaningful.length === 0) {
133
- if (body.startPosition.row > start) {
134
- state.ellipsisAfter.add(body.startPosition.row - 1);
135
- } else if (!state.replace.has(start)) {
136
- state.replace.set(start, collapseOneLineBody(state.lines[start]));
137
- }
138
- return;
139
- }
140
- for (const st of body.namedChildren) {
141
- processStatement(state, st);
142
- }
143
- return;
144
- }
145
- case 'expression_statement': {
146
- const hasAssign = node.namedChildren.some(
147
- (c: SyntaxNode) => c.type === 'assignment',
148
- );
149
- if (hasAssign) {
150
- addRange(state, node.startPosition.row, node.endPosition.row);
151
- }
152
- return;
153
- }
154
- default:
155
- return;
156
- }
157
- }
158
-
159
- function buildSkeletonAst(content: string): string {
160
- const parser = getParser();
161
- if (!parser) return content.split('\n').slice(0, 50).join('\n');
162
- const tree = parser.parse(content);
163
- const lines = content.split(/\r?\n/);
164
- const state: SkeletonState = {
165
- lines,
166
- keep: new Set<number>(),
167
- replace: new Map<number, string>(),
168
- ellipsisAfter: new Set<number>(),
169
- };
170
-
171
- if (tree.rootNode.type === 'module') {
172
- for (const child of tree.rootNode.namedChildren) {
173
- processStatement(state, child);
174
- }
175
- }
176
-
177
- const out: string[] = [];
178
- for (let i = 0; i < lines.length; i++) {
179
- if (state.replace.has(i)) {
180
- out.push(state.replace.get(i)!);
181
- continue;
182
- }
183
- if (state.keep.has(i)) {
184
- out.push(lines[i]);
185
- if (state.ellipsisAfter.has(i)) {
186
- const ind = baseIndent(lines[i]);
187
- out.push(`${ind} ...`);
188
- }
189
- }
190
- }
191
-
192
- return out.join('\n').trim();
193
- }
194
-
195
- function walkImports(node: SyntaxNode, acc: Set<string>): void {
196
- if (node.type === 'import_from_statement') {
197
- const mod = node.childForFieldName('module_name');
198
- if (mod) {
199
- acc.add(mod.text);
200
- }
201
- } else if (node.type === 'import_statement') {
202
- for (const child of node.namedChildren) {
203
- if (child.type === 'dotted_name') {
204
- acc.add(child.text);
205
- } else if (child.type === 'aliased_import') {
206
- const n = child.childForFieldName('name');
207
- if (n?.type === 'dotted_name') {
208
- acc.add(n.text);
209
- }
210
- }
211
- }
212
- }
213
- for (const child of node.namedChildren) {
214
- walkImports(child, acc);
215
- }
216
- }
217
-
218
- function walkSymbols(node: SyntaxNode, symbols: Symbol[]): void {
219
- if (node.type === 'function_definition') {
220
- const nameNode = node.childForFieldName('name');
221
- if (nameNode) {
222
- symbols.push({
223
- name: nameNode.text,
224
- kind: 'function',
225
- line: node.startPosition.row + 1,
226
- });
227
- }
228
- } else if (node.type === 'class_definition') {
229
- const nameNode = node.childForFieldName('name');
230
- if (nameNode) {
231
- symbols.push({
232
- name: nameNode.text,
233
- kind: 'class',
234
- line: node.startPosition.row + 1,
235
- });
236
- }
237
- } else if (node.type === 'type_alias_statement') {
238
- const left = node.childForFieldName('left');
239
- if (left) {
240
- symbols.push({
241
- name: left.text,
242
- kind: 'type',
243
- line: node.startPosition.row + 1,
244
- });
245
- }
246
- } else if (node.type === 'assignment') {
247
- const left = node.childForFieldName('left');
248
- if (left?.type === 'identifier') {
249
- symbols.push({
250
- name: left.text,
251
- kind: 'variable',
252
- line: node.startPosition.row + 1,
253
- });
254
- }
255
- }
256
- for (const child of node.namedChildren) {
257
- walkSymbols(child, symbols);
258
- }
259
- }
260
-
261
- export class PythonAdapter implements LanguageAdapter {
262
- name = 'python';
263
- extensions = ['.py', '.pyi'];
264
-
265
- canHandle(filePath: string): boolean {
266
- return this.extensions.includes(path.extname(filePath).toLowerCase());
267
- }
268
-
269
- generateSkeleton(content: string, _filePath: string, _rootDir?: string): SkeletonResult {
270
- try {
271
- return { skeleton: buildSkeletonAst(content) };
272
- } catch {
273
- return { skeleton: content.split('\n').slice(0, 50).join('\n') };
274
- }
275
- }
276
-
277
- extractSymbols(content: string): Symbol[] {
278
- try {
279
- const parser = getParser();
280
- if (!parser) return [];
281
- const tree = parser.parse(content);
282
- const symbols: Symbol[] = [];
283
- walkSymbols(tree.rootNode, symbols);
284
- return symbols;
285
- } catch {
286
- return [];
287
- }
288
- }
289
-
290
- extractImports(content: string): string[] {
291
- try {
292
- const parser = getParser();
293
- if (!parser) return [];
294
- const tree = parser.parse(content);
295
- const mods = new Set<string>();
296
- walkImports(tree.rootNode, mods);
297
- return [...mods];
298
- } catch {
299
- return [];
300
- }
301
- }
302
- }
@@ -1,81 +0,0 @@
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
- });
@@ -1,31 +0,0 @@
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
- });