Add evaluation framework and fix call graph extraction

- Add evaluation test suite with TypeScript and Python fixtures
- Fix MCP server to defer CodeGraph init until rootUri received
- Fix call edge extraction by calling resolveReferences() after indexAll/sync
- Fix glob matching for root-level files (e.g., **/*.py now matches auth.py)
- Fix duplicate node extraction for methods inside classes
- Update context tests to use buildContext for semantic search + graph traversal
- Export unused formatter functions to fix build

Evaluation results:
- TypeScript: 96% precision, 79% recall, 85% F1
- Python: 99% precision, 80% recall, 85% F1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-01-18 18:48:22 -06:00
co-authored by Claude Opus 4.5
parent e306114607
commit 6b672f9152
30 changed files with 2600 additions and 129 deletions
+18 -7
View File
@@ -65,13 +65,24 @@ export function hashContent(content: string): string {
* Check if a path matches any glob pattern (simplified)
*/
function matchesGlob(filePath: string, pattern: string): boolean {
// Convert glob to regex (simplified)
const regexStr = pattern
.replace(/\./g, '\\.')
.replace(/\*\*/g, '<<<GLOBSTAR>>>')
.replace(/\*/g, '[^/]*')
.replace(/<<<GLOBSTAR>>>/g, '.*')
.replace(/\?/g, '.');
// Convert glob to regex using placeholders to avoid conflicts
let regexStr = pattern;
// Replace glob patterns with placeholders first
regexStr = regexStr.replace(/\*\*\//g, '\x00GLOBSTAR_SLASH\x00');
regexStr = regexStr.replace(/\*\*/g, '\x00GLOBSTAR\x00');
regexStr = regexStr.replace(/\*/g, '\x00STAR\x00');
regexStr = regexStr.replace(/\?/g, '\x00QUESTION\x00');
// Escape regex special characters
regexStr = regexStr.replace(/[.+^${}()|[\]\\]/g, '\\$&');
// Replace placeholders with regex equivalents
regexStr = regexStr.replace(/\x00GLOBSTAR_SLASH\x00/g, '(?:.*/)?'); // **/ = zero or more dirs
regexStr = regexStr.replace(/\x00GLOBSTAR\x00/g, '.*'); // ** = anything
regexStr = regexStr.replace(/\x00STAR\x00/g, '[^/]*'); // * = anything except /
regexStr = regexStr.replace(/\x00QUESTION\x00/g, '.'); // ? = single char
const regex = new RegExp(`^${regexStr}$`);
return regex.test(filePath);
}
+23 -7
View File
@@ -743,10 +743,19 @@ export class TreeSitterExtractor {
if (!this.extractor) return;
const nodeType = node.type;
let skipChildren = false;
// Check for function declarations
// For Python/Ruby, function_definition inside a class should be treated as method
if (this.extractor.functionTypes.includes(nodeType)) {
this.extractFunction(node);
if (this.nodeStack.length > 0 && this.extractor.methodTypes.includes(nodeType)) {
// Inside a class - treat as method
this.extractMethod(node);
skipChildren = true; // extractMethod visits children via visitFunctionBody
} else {
this.extractFunction(node);
skipChildren = true; // extractFunction visits children via visitFunctionBody
}
}
// Check for class declarations
else if (this.extractor.classTypes.includes(nodeType)) {
@@ -759,22 +768,27 @@ export class TreeSitterExtractor {
} else {
this.extractClass(node);
}
skipChildren = true; // extractClass visits body children
}
// Check for method declarations
// Check for method declarations (only if not already handled by functionTypes)
else if (this.extractor.methodTypes.includes(nodeType)) {
this.extractMethod(node);
skipChildren = true; // extractMethod visits children via visitFunctionBody
}
// Check for interface/protocol/trait declarations
else if (this.extractor.interfaceTypes.includes(nodeType)) {
this.extractInterface(node);
skipChildren = true; // extractInterface visits body children
}
// Check for struct declarations
else if (this.extractor.structTypes.includes(nodeType)) {
this.extractStruct(node);
skipChildren = true; // extractStruct visits body children
}
// Check for enum declarations
else if (this.extractor.enumTypes.includes(nodeType)) {
this.extractEnum(node);
skipChildren = true; // extractEnum visits body children
}
// Check for imports
else if (this.extractor.importTypes.includes(nodeType)) {
@@ -785,11 +799,13 @@ export class TreeSitterExtractor {
this.extractCall(node);
}
// Visit children
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) {
this.visitNode(child);
// Visit children (unless the extract method already visited them)
if (!skipChildren) {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) {
this.visitNode(child);
}
}
}
}