Port bug fixes and stability improvements from PR #15

- Fix Float32Array embedder bug: was creating zero-filled array instead
  of copying data from TypedArray-like objects
- Fix VSS search query: use subquery pattern so LIMIT applies before JOIN
- Pin tree-sitter versions: remove caret ranges for ABI stability, add
  overrides to lock tree-sitter core at 0.22.4
- Lazy grammar loading: load native bindings on first use per language
  instead of all at startup, so one missing grammar doesn't affect others
- Remove stale src/extraction/queries copy from copy-assets script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-02-09 23:40:37 -06:00
co-authored by Claude Opus 4.6
parent 082513b566
commit e07250ed60
4 changed files with 159 additions and 99 deletions
+19 -16
View File
@@ -15,7 +15,7 @@
"scripts": { "scripts": {
"build": "tsc && npm run copy-assets", "build": "tsc && npm run copy-assets",
"postinstall": "node scripts/postinstall.js", "postinstall": "node scripts/postinstall.js",
"copy-assets": "node -e \"const fs=require('fs'),p=require('path');function cpR(s,d){if(!fs.existsSync(s))return;fs.mkdirSync(d,{recursive:true});for(const f of fs.readdirSync(s)){const sp=p.join(s,f),dp=p.join(d,f);fs.statSync(sp).isDirectory()?cpR(sp,dp):fs.copyFileSync(sp,dp)}}cpR('src/extraction/queries','dist/extraction/queries');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql')\"", "copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql')\"",
"dev": "tsc --watch", "dev": "tsc --watch",
"cli": "npm run build && node dist/bin/codegraph.js", "cli": "npm run build && node dist/bin/codegraph.js",
"test": "vitest run", "test": "vitest run",
@@ -38,22 +38,22 @@
"commander": "^14.0.2", "commander": "^14.0.2",
"figlet": "^1.8.0", "figlet": "^1.8.0",
"sqlite-vss": "^0.1.2", "sqlite-vss": "^0.1.2",
"tree-sitter": "^0.22.4", "tree-sitter": "0.22.4",
"tree-sitter-c": "^0.23.4", "tree-sitter-c": "0.23.4",
"tree-sitter-c-sharp": "^0.23.1", "tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "^0.23.4", "tree-sitter-cpp": "0.23.4",
"@sengac/tree-sitter-dart": "^1.1.6", "@sengac/tree-sitter-dart": "1.1.6",
"tree-sitter-go": "^0.23.4", "tree-sitter-go": "0.23.4",
"tree-sitter-java": "^0.23.5", "tree-sitter-java": "0.23.5",
"tree-sitter-javascript": "^0.23.1", "tree-sitter-javascript": "0.23.1",
"tree-sitter-kotlin": "^0.3.8", "tree-sitter-kotlin": "0.3.8",
"tree-sitter-liquid": "github:hankthetank27/tree-sitter-liquid", "tree-sitter-liquid": "github:hankthetank27/tree-sitter-liquid",
"tree-sitter-php": "^0.23.11", "tree-sitter-php": "0.23.11",
"tree-sitter-python": "^0.23.6", "tree-sitter-python": "0.23.6",
"tree-sitter-ruby": "^0.23.1", "tree-sitter-ruby": "0.23.1",
"tree-sitter-rust": "^0.23.2", "tree-sitter-rust": "0.23.2",
"tree-sitter-swift": "^0.7.1", "tree-sitter-swift": "0.7.1",
"tree-sitter-typescript": "^0.23.2" "tree-sitter-typescript": "0.23.2"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.0", "@types/better-sqlite3": "^7.6.0",
@@ -64,5 +64,8 @@
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
},
"overrides": {
"tree-sitter": "0.22.4"
} }
} }
+131 -75
View File
@@ -1,73 +1,88 @@
/** /**
* Grammar Loading and Caching * Grammar Loading and Caching
* *
* Manages tree-sitter language grammars. * Uses lazy per-language loading so one missing native grammar does not
* break extraction for all other languages.
*/ */
import Parser from 'tree-sitter'; import Parser from 'tree-sitter';
import { Language } from '../types'; import { Language } from '../types';
// Grammar module imports — wrapped in tryRequire so a missing native binding type GrammarLoader = () => unknown;
// (e.g. tree-sitter-kotlin on Windows) degrades gracefully instead of crashing. type GrammarLanguage = Exclude<Language, 'liquid' | 'unknown'>;
// eslint-disable-next-line @typescript-eslint/no-require-imports
function tryRequire(id: string, prop?: string): unknown | null {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require(id);
return prop ? mod[prop] : mod;
} catch {
console.warn(`[CodeGraph] Failed to load ${id}${prop ?? id} parsing will be unavailable on this platform.`);
return null;
}
}
const TypeScript = tryRequire('tree-sitter-typescript', 'typescript');
const TSX = tryRequire('tree-sitter-typescript', 'tsx');
const JavaScript = tryRequire('tree-sitter-javascript');
const Python = tryRequire('tree-sitter-python');
const Go = tryRequire('tree-sitter-go');
const Rust = tryRequire('tree-sitter-rust');
const Java = tryRequire('tree-sitter-java');
const C = tryRequire('tree-sitter-c');
const Cpp = tryRequire('tree-sitter-cpp');
const CSharp = tryRequire('tree-sitter-c-sharp');
const PHP = tryRequire('tree-sitter-php', 'php');
const Ruby = tryRequire('tree-sitter-ruby');
const Swift = tryRequire('tree-sitter-swift');
const Kotlin = tryRequire('tree-sitter-kotlin');
const Dart = tryRequire('@sengac/tree-sitter-dart');
// Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
// Liquid extraction is handled separately via regex in tree-sitter.ts
/** /**
* Mapping of Language to tree-sitter grammar. * Lazy grammar loaders — each language's native binding is only loaded
* Parsers that failed to load are excluded. * on first use, so a failure in one grammar doesn't affect others.
*/ */
const GRAMMAR_MAP: Record<string, unknown> = {}; const grammarLoaders: Record<GrammarLanguage, GrammarLoader> = {
typescript: () => {
const grammarEntries: [string, unknown][] = [ // eslint-disable-next-line @typescript-eslint/no-require-imports
['typescript', TypeScript], return require('tree-sitter-typescript').typescript;
['tsx', TSX], },
['javascript', JavaScript], tsx: () => {
['jsx', JavaScript], // JSX uses the JavaScript grammar // eslint-disable-next-line @typescript-eslint/no-require-imports
['python', Python], return require('tree-sitter-typescript').tsx;
['go', Go], },
['rust', Rust], javascript: () => {
['java', Java], // eslint-disable-next-line @typescript-eslint/no-require-imports
['c', C], return require('tree-sitter-javascript');
['cpp', Cpp], },
['csharp', CSharp], jsx: () => {
['php', PHP], // eslint-disable-next-line @typescript-eslint/no-require-imports
['ruby', Ruby], return require('tree-sitter-javascript');
['swift', Swift], },
['kotlin', Kotlin], python: () => {
['dart', Dart], // eslint-disable-next-line @typescript-eslint/no-require-imports
// liquid: uses custom regex-based extraction, not tree-sitter return require('tree-sitter-python');
]; },
go: () => {
for (const [lang, grammar] of grammarEntries) { // eslint-disable-next-line @typescript-eslint/no-require-imports
if (grammar) GRAMMAR_MAP[lang] = grammar; return require('tree-sitter-go');
} },
rust: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-rust');
},
java: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-java');
},
c: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-c');
},
cpp: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-cpp');
},
csharp: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-c-sharp');
},
php: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-php').php;
},
ruby: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-ruby');
},
swift: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-swift');
},
kotlin: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('tree-sitter-kotlin');
},
dart: () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('@sengac/tree-sitter-dart');
},
// Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
// Liquid extraction is handled separately via regex in tree-sitter.ts
};
/** /**
* File extension to Language mapping * File extension to Language mapping
@@ -103,30 +118,59 @@ export const EXTENSION_MAP: Record<string, Language> = {
}; };
/** /**
* Cache for initialized parsers * Caches for loaded grammars and parsers
*/ */
const parserCache = new Map<Language, Parser>(); const parserCache = new Map<Language, Parser>();
const grammarCache = new Map<Language, unknown | null>();
const unavailableGrammarErrors = new Map<Language, string>();
/**
* Load a grammar on demand, caching the result.
* Returns null if the grammar is not available on this platform.
*/
function loadGrammar(language: Language): unknown | null {
if (grammarCache.has(language)) {
return grammarCache.get(language) ?? null;
}
const loader = grammarLoaders[language as GrammarLanguage];
if (!loader) {
grammarCache.set(language, null);
return null;
}
try {
const grammar = loader();
if (!grammar) {
throw new Error(`Grammar loader returned empty value for ${language}`);
}
grammarCache.set(language, grammar);
return grammar;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`[CodeGraph] Failed to load ${language} grammar — parsing will be unavailable: ${message}`);
unavailableGrammarErrors.set(language, message);
grammarCache.set(language, null);
return null;
}
}
/** /**
* Get a parser for the specified language * Get a parser for the specified language
*/ */
export function getParser(language: Language): Parser | null { export function getParser(language: Language): Parser | null {
// Check cache first
if (parserCache.has(language)) { if (parserCache.has(language)) {
return parserCache.get(language)!; return parserCache.get(language)!;
} }
// Get grammar for language const grammar = loadGrammar(language);
const grammar = GRAMMAR_MAP[language];
if (!grammar) { if (!grammar) {
return null; return null;
} }
// Create and cache parser
const parser = new Parser(); const parser = new Parser();
parser.setLanguage(grammar as Parameters<typeof parser.setLanguage>[0]); parser.setLanguage(grammar as Parameters<typeof parser.setLanguage>[0]);
parserCache.set(language, parser); parserCache.set(language, parser);
return parser; return parser;
} }
@@ -139,29 +183,41 @@ export function detectLanguage(filePath: string): Language {
} }
/** /**
* Check if a language is supported * Check if a language is supported by currently available parsers.
*/ */
export function isLanguageSupported(language: Language): boolean { export function isLanguageSupported(language: Language): boolean {
// Liquid uses custom regex-based extraction, not tree-sitter if (language === 'liquid') return true; // custom regex extractor
if (language === 'liquid') return true; if (language === 'unknown') return false;
return language !== 'unknown' && language in GRAMMAR_MAP; return loadGrammar(language) !== null;
} }
/** /**
* Get all supported languages * Get all currently supported languages.
*/ */
export function getSupportedLanguages(): Language[] { export function getSupportedLanguages(): Language[] {
const languages = Object.keys(GRAMMAR_MAP) as Language[]; const available = (Object.keys(grammarLoaders) as GrammarLanguage[])
// Add Liquid which uses custom extraction .filter((language) => loadGrammar(language) !== null);
languages.push('liquid'); return [...available, 'liquid'];
return languages;
} }
/** /**
* Clear the parser cache (useful for testing) * Clear parser/grammar caches (useful for testing)
*/ */
export function clearParserCache(): void { export function clearParserCache(): void {
parserCache.clear(); parserCache.clear();
grammarCache.clear();
unavailableGrammarErrors.clear();
}
/**
* Report grammars that failed to load.
*/
export function getUnavailableGrammarErrors(): Partial<Record<Language, string>> {
const out: Partial<Record<Language, string>> = {};
for (const [language, message] of unavailableGrammarErrors.entries()) {
out[language] = message;
}
return out;
} }
/** /**
+1 -1
View File
@@ -296,7 +296,7 @@ export class TextEmbedder {
if (data && typeof data === 'object' && 'length' in data) { if (data && typeof data === 'object' && 'length' in data) {
// Handle TypedArray-like objects // Handle TypedArray-like objects
const arr = data as ArrayLike<number>; const arr = data as ArrayLike<number>;
return new Float32Array(arr.length); return Float32Array.from(Array.from(arr));
} }
throw new Error('Unsupported data format for embedding'); throw new Error('Unsupported data format for embedding');
} }
+8 -7
View File
@@ -324,13 +324,14 @@ export class VectorSearchManager {
const rows = this.db const rows = this.db
.prepare( .prepare(
` `
SELECT SELECT m.node_id, v.distance
vss_map.node_id, FROM (
vss_vectors.distance SELECT rowid, distance
FROM vss_vectors FROM vss_vectors
JOIN vss_map ON vss_map.rowid = vss_vectors.rowid WHERE vss_search(embedding, ?)
WHERE vss_search(vss_vectors.embedding, ?) LIMIT ${safeLimit}
LIMIT ${safeLimit} ) v
JOIN vss_map m ON m.rowid = v.rowid
` `
) )
.all(vectorJson) as Array<{ node_id: string; distance: number }>; .all(vectorJson) as Array<{ node_id: string; distance: number }>;