Enhances code extraction and project indexing

Adds support for Dart and Liquid languages with tree-sitter parsing.
Improves accuracy of code symbol extraction for existing languages.
Indexes project files to enhance code navigation features.
Migrates build system to facilitate code contributions.
Removes git hook functionality.
Integrates Sentry for error tracking and reporting.
Enhances project initialization and configuration loading.
This commit is contained in:
Colby McHenry
2026-02-09 22:18:59 -06:00
parent f0ddfccf47
commit d0ee6f7fc4
50 changed files with 3428 additions and 3332 deletions
+6
View File
@@ -38,6 +38,9 @@ export class DatabaseConnection {
// Enable foreign keys and WAL mode for better performance
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
// Wait up to 2 minutes if database is locked by another process
// (indexing operations can hold locks for extended periods)
db.pragma('busy_timeout = 120000');
// Run schema initialization
const schemaPath = path.join(__dirname, 'schema.sql');
@@ -60,6 +63,9 @@ export class DatabaseConnection {
// Enable foreign keys and WAL mode
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
// Wait up to 2 minutes if database is locked by another process
// (indexing operations can hold locks for extended periods)
db.pragma('busy_timeout = 120000');
// Check and run migrations if needed
const conn = new DatabaseConnection(db, dbPath);
+110 -28
View File
@@ -198,28 +198,54 @@ export class QueryBuilder {
`);
}
this.stmts.insertNode.run({
id: node.id,
kind: node.kind,
name: node.name,
qualifiedName: node.qualifiedName,
filePath: node.filePath,
language: node.language,
startLine: node.startLine,
endLine: node.endLine,
startColumn: node.startColumn,
endColumn: node.endColumn,
docstring: node.docstring ?? null,
signature: node.signature ?? null,
visibility: node.visibility ?? null,
isExported: node.isExported ? 1 : 0,
isAsync: node.isAsync ? 1 : 0,
isStatic: node.isStatic ? 1 : 0,
isAbstract: node.isAbstract ? 1 : 0,
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
updatedAt: node.updatedAt,
});
// Validate required fields to prevent SQLite bind errors
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
console.error('[CodeGraph] Skipping node with missing required fields:', {
id: node.id,
kind: node.kind,
name: node.name,
filePath: node.filePath,
language: node.language,
});
return;
}
try {
this.stmts.insertNode.run({
id: node.id,
kind: node.kind,
name: node.name,
qualifiedName: node.qualifiedName ?? node.name,
filePath: node.filePath,
language: node.language,
startLine: node.startLine ?? 0,
endLine: node.endLine ?? 0,
startColumn: node.startColumn ?? 0,
endColumn: node.endColumn ?? 0,
docstring: node.docstring ?? null,
signature: node.signature ?? null,
visibility: node.visibility ?? null,
isExported: node.isExported ? 1 : 0,
isAsync: node.isAsync ? 1 : 0,
isStatic: node.isStatic ? 1 : 0,
isAbstract: node.isAbstract ? 1 : 0,
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
updatedAt: node.updatedAt ?? Date.now(),
});
} catch (error) {
const { captureException } = require('../sentry');
captureException(error, {
operation: 'insertNode',
nodeId: node.id,
nodeKind: node.kind,
nodeName: node.name,
filePath: node.filePath,
language: node.language,
startLine: node.startLine,
});
throw error;
}
}
/**
@@ -266,17 +292,23 @@ export class QueryBuilder {
// Invalidate cache before update
this.nodeCache.delete(node.id);
// Validate required fields
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
console.error('[CodeGraph] Skipping node update with missing required fields:', node.id);
return;
}
this.stmts.updateNode.run({
id: node.id,
kind: node.kind,
name: node.name,
qualifiedName: node.qualifiedName,
qualifiedName: node.qualifiedName ?? node.name,
filePath: node.filePath,
language: node.language,
startLine: node.startLine,
endLine: node.endLine,
startColumn: node.startColumn,
endColumn: node.endColumn,
startLine: node.startLine ?? 0,
endLine: node.endLine ?? 0,
startColumn: node.startColumn ?? 0,
endColumn: node.endColumn ?? 0,
docstring: node.docstring ?? null,
signature: node.signature ?? null,
visibility: node.visibility ?? null,
@@ -286,7 +318,7 @@ export class QueryBuilder {
isAbstract: node.isAbstract ? 1 : 0,
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
updatedAt: node.updatedAt,
updatedAt: node.updatedAt ?? Date.now(),
});
}
@@ -524,6 +556,56 @@ export class QueryBuilder {
}));
}
/**
* Find nodes by exact name match
*
* Used for hybrid search - looks up symbols by exact name or case-insensitive match.
* Returns high-confidence matches for known symbol names extracted from query.
*
* @param names - Array of symbol names to look up
* @param options - Search options (kinds, languages, limit)
* @returns SearchResult array with exact matches scored at 1.0
*/
findNodesByExactName(names: string[], options: SearchOptions = {}): SearchResult[] {
if (names.length === 0) return [];
const { kinds, languages, limit = 50 } = options;
// Build query with exact matches (case-insensitive)
let sql = `
SELECT nodes.*,
CASE
WHEN name COLLATE NOCASE IN (${names.map(() => '?').join(',')}) THEN 1.0
ELSE 0.9
END as score
FROM nodes
WHERE name COLLATE NOCASE IN (${names.map(() => '?').join(',')})
`;
// Duplicate names for both SELECT and WHERE clauses
const params: (string | number)[] = [...names, ...names];
if (kinds && kinds.length > 0) {
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
params.push(...kinds);
}
if (languages && languages.length > 0) {
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
params.push(...languages);
}
sql += ' ORDER BY score DESC, length(name) ASC LIMIT ?';
params.push(limit);
const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
return rows.map((row) => ({
node: rowToNode(row),
score: row.score,
}));
}
// ===========================================================================
// Edge Operations
// ===========================================================================
+10 -9
View File
@@ -89,32 +89,33 @@ CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path);
CREATE INDEX IF NOT EXISTS idx_nodes_language ON nodes(language);
CREATE INDEX IF NOT EXISTS idx_nodes_file_line ON nodes(file_path, start_line);
-- Full-text search index on node names and docstrings
-- Full-text search index on node names, docstrings, and signatures
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
id,
name,
qualified_name,
docstring,
signature,
content='nodes',
content_rowid='rowid'
);
-- Triggers to keep FTS index in sync
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring)
VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring);
INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature)
VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature);
END;
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring)
VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring);
INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature)
VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature);
END;
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring)
VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring);
INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring)
VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring);
INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature)
VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature);
INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature)
VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature);
END;
-- Edge indexes