ai_api / src /lib /copilot /codegraphKnowledge.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
7.85 kB
/**
* Copilot CodeGraph Knowledge Module
*
* Provides the Copilot with read-only access to the project's CodeGraph index.
* Queries the `.codegraph/codegraph.db` SQLite database to find symbols,
* explore relationships, list files, and search documentation.
*
* Falls back gracefully if the CodeGraph DB does not exist (e.g., production installs).
*/
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface CodeGraphNode {
id: string;
kind: string;
name: string;
qualifiedName: string;
filePath: string;
language: string;
startLine: number;
endLine: number;
signature?: string;
docstring?: string;
isExported: boolean;
visibility?: string;
}
export interface CodeGraphEdge {
id: number;
source: string;
target: string;
kind: string;
line?: number;
metadata?: Record<string, unknown>;
}
export interface CodeGraphFile {
path: string;
language: string;
nodeCount: number;
modifiedAt: number;
}
export interface CodeGraphSearchResult {
nodes: CodeGraphNode[];
total: number;
}
// ---------------------------------------------------------------------------
// Database access (lazy loaded)
// ---------------------------------------------------------------------------
let _db: unknown = null;
function getDbPath(): string | null {
// Try project root first (dev), then cwd, then DATA_DIR
const candidates = [
join(process.cwd(), ".codegraph", "codegraph.db"),
join(process.cwd(), "..", ".codegraph", "codegraph.db"),
];
// Try to resolve from the project root
const __dirname = dirname(fileURLToPath(import.meta.url));
// Walk up to find .codegraph/
let dir = __dirname;
for (let i = 0; i < 10; i++) {
const candidate = join(dir, ".codegraph", "codegraph.db");
if (existsSync(candidate)) return candidate;
const parent = join(dir, "..");
if (parent === dir) break;
dir = parent;
}
for (const c of candidates) {
if (existsSync(c)) return c;
}
return null;
}
export interface CodeGraphQueryResult {
success: boolean;
data: unknown;
error?: string;
engine: "sqlite" | "cli" | "none";
}
function queryDb(query: string, params: unknown[] = []): CodeGraphQueryResult {
try {
if (!_db) {
const dbPath = getDbPath();
if (!dbPath) {
return { success: false, data: null, error: "CodeGraph DB not found", engine: "none" };
}
// Dynamic import to avoid hard dependency on better-sqlite3
_db = null;
// Use better-sqlite3 if available
try {
const Database = require("better-sqlite3");
_db = new Database(dbPath, { readonly: true });
} catch {
return {
success: false,
data: null,
error: "better-sqlite3 not available",
engine: "none",
};
}
}
const stmt = (
_db as { prepare: (sql: string) => { all: (params: unknown[]) => unknown[] } }
).prepare(query);
const rows = stmt.all(params);
return { success: true, data: rows, engine: "sqlite" };
} catch (err) {
return {
success: false,
data: null,
error: err instanceof Error ? err.message : "Unknown error",
engine: "none",
};
}
}
// ---------------------------------------------------------------------------
// Search operations
// ---------------------------------------------------------------------------
/**
* Search symbols by name (exact or partial match via FTS).
*/
export function searchSymbols(query: string, limit = 20): CodeGraphQueryResult {
const sql = `
SELECT n.*
FROM nodes n
JOIN nodes_fts fts ON n.id = fts.id
WHERE nodes_fts MATCH ?
ORDER BY rank
LIMIT ?
`;
// Escape FTS special chars and create prefix query
const sanitized = query.replace(/[^a-zA-Z0-9_]/g, " ").trim();
if (!sanitized) {
// Fallback to LIKE if query is empty after sanitization
return queryDb(`SELECT * FROM nodes WHERE lower(name) LIKE ? ORDER BY kind, name LIMIT ?`, [
`%${query.toLowerCase()}%`,
limit,
]);
}
const ftsQuery = sanitized
.split(/\s+/)
.map((w) => `"${w}"*`)
.join(" AND ");
return queryDb(sql, [ftsQuery, limit]);
}
/**
* Find callers of a symbol (edges where target matches).
*/
export function findCallers(symbolName: string, limit = 20): CodeGraphQueryResult {
return queryDb(
`SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col,
s.id as sourceId, s.name as sourceName, s.kind as sourceKind,
s.file_path as sourceFile, s.start_line as sourceLine,
t.name as targetName, t.file_path as targetFile
FROM edges e
JOIN nodes s ON e.source = s.id
JOIN nodes t ON e.target = t.id
WHERE t.name = ?
ORDER BY e.kind
LIMIT ?`,
[symbolName, limit]
);
}
/**
* Find callees of a symbol (edges where source matches).
*/
export function findCallees(symbolName: string, limit = 20): CodeGraphQueryResult {
return queryDb(
`SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col,
s.name as sourceName,
t.id as targetId, t.name as targetName, t.kind as targetKind,
t.file_path as targetFile, t.start_line as targetLine
FROM edges e
JOIN nodes s ON e.source = s.id
JOIN nodes t ON e.target = t.id
WHERE s.name = ?
ORDER BY e.kind
LIMIT ?`,
[symbolName, limit]
);
}
/**
* Get context for a file: all symbols defined in it.
*/
export function getFileContext(filePath: string): CodeGraphQueryResult {
// Try matching on suffix of file_path (many nodes store paths relative to root)
return queryDb(
`SELECT * FROM nodes
WHERE file_path LIKE ? OR file_path = ?
ORDER BY start_line
LIMIT 100`,
[`%${filePath}`, filePath]
);
}
/**
* List all indexed files, optionally filtered by language.
*/
export function listFiles(language?: string, limit = 50): CodeGraphQueryResult {
if (language) {
return queryDb(`SELECT * FROM files WHERE language = ? ORDER BY path LIMIT ?`, [
language,
limit,
]);
}
return queryDb(`SELECT * FROM files ORDER BY path LIMIT ?`, [limit]);
}
/**
* Get impact analysis: find symbols that depend on a given symbol (transitively).
*/
export function getImpactAnalysis(symbolName: string, depth = 1): CodeGraphQueryResult {
if (depth <= 0)
return { success: false, data: null, error: "Depth must be >= 1", engine: "none" };
// Direct callers (depth 1)
const directCallers = findCallers(symbolName);
if (depth === 1) return directCallers;
// For depth > 1, we'd need recursive CTE or multiple queries.
// For now, just return direct callers with a note.
const result = directCallers;
return {
...result,
data: (result.data as Record<string, unknown>[])?.map((r) => ({
...r,
_depth: 1,
_note: `Depth > 1 requires multiple queries. Use searchSymbols() + findCallers() iteratively for deeper analysis.`,
})),
};
}
/**
* Check if CodeGraph DB is available.
*/
export function isCodeGraphAvailable(): boolean {
return getDbPath() !== null;
}
/**
* Get summary stats from the index.
*/
export function getCodeGraphStats(): CodeGraphQueryResult {
return queryDb(`SELECT 'total_nodes' as key, COUNT(*) as value FROM nodes UNION ALL
SELECT 'total_edges', COUNT(*) FROM edges UNION ALL
SELECT 'total_files', COUNT(*) FROM files UNION ALL
SELECT 'languages', GROUP_CONCAT(DISTINCT language) FROM files UNION ALL
SELECT 'node_kinds', GROUP_CONCAT(DISTINCT kind) FROM nodes`);
}