Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ All notable changes to this project will be documented in this file. See [commit
* add `npm run bench` script and stale embeddings warning ([#604](https://github.com/optave/codegraph/pull/604))
* bump `commit-and-tag-version`, `tree-sitter-cli`, `web-tree-sitter`, `@commitlint/cli`, `@commitlint/config-conventional` ([#560](https://github.com/optave/codegraph/pull/560), [#561](https://github.com/optave/codegraph/pull/561), [#562](https://github.com/optave/codegraph/pull/562), [#563](https://github.com/optave/codegraph/pull/563), [#564](https://github.com/optave/codegraph/pull/564))

### Notes

* **constants:** `EXTENSIONS` and `IGNORE_DIRS` in the programmatic API are now `Set<string>` (changed during TypeScript migration). Both expose a `.toArray()` convenience method for consumers that need array semantics.

## [3.3.1](https://github.com/optave/codegraph/compare/v3.3.0...v3.3.1) (2026-03-20)

**Incremental rebuild accuracy and post-3.3.0 stabilization.** This patch fixes a critical edge gap in the file watcher's single-file rebuild path where call edges were silently dropped during incremental rebuilds, aligns the native Rust engine's edge builder kind filters with the JS engine for parity, plugs a WASM tree memory leak in native engine typeMap backfill, and restores query performance to pre-3.1.4 levels. Several post-reorganization import path issues are also corrected.
Expand Down
20 changes: 11 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"@commitlint/config-conventional": "^20.0",
"@huggingface/transformers": "^3.8.1",
"@tree-sitter-grammars/tree-sitter-hcl": "^1.2.0",
"@types/better-sqlite3": "^7.6.13",
"@vitest/coverage-v8": "^4.0.18",
"commit-and-tag-version": "^12.5",
"husky": "^9.1",
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export const command: CommandDefinition = {
const dbPath = findDbPath();
const fs = await import('node:fs');
if (fs.existsSync(dbPath)) {
// @ts-expect-error -- better-sqlite3 default export typing
const db = new Database(dbPath, { readonly: true });
const buildEngine = getBuildMeta(db, 'engine');
const buildVersion = getBuildMeta(db, 'codegraph_version');
Expand Down
4 changes: 2 additions & 2 deletions src/cli/shared/open-graph.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type Database from 'better-sqlite3';
import { openReadonlyOrFail } from '../../db/index.js';
import type { BetterSqlite3Database } from '../../types.js';

/**
* Open the graph database in readonly mode with a clean close() handle.
*/
export function openGraph(opts: { db?: string } = {}): {
db: Database.Database;
db: BetterSqlite3Database;
close: () => void;
} {
const db = openReadonlyOrFail(opts.db);
Expand Down
55 changes: 47 additions & 8 deletions src/db/connection.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3';
import { debug, warn } from '../infrastructure/logger.js';
import { DbError } from '../shared/errors.js';
import type { BetterSqlite3Database } from '../types.js';
import { Repository } from './repository/base.js';
import { SqliteRepository } from './repository/sqlite-repository.js';

/** Lazy-loaded package version (read once from package.json). */
let _packageVersion: string | undefined;
function getPackageVersion(): string {
if (_packageVersion !== undefined) return _packageVersion;
try {
const connDir = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.join(connDir, '..', '..', 'package.json');
_packageVersion = (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string })
.version;
} catch {
_packageVersion = '';
}
return _packageVersion;
}

/** Warn once per process when DB version mismatches the running codegraph version. */
let _versionWarned = false;

/** DB instance with optional advisory lock path. */
export type LockedDatabase = BetterSqlite3Database & { __lockPath?: string };

Expand Down Expand Up @@ -60,6 +79,11 @@ export function _resetRepoRootCache(): void {
_cachedRepoRootCwd = undefined;
}

/** Reset the version warning flag (for testing). */
export function _resetVersionWarning(): void {
_versionWarned = false;
}

function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
Expand Down Expand Up @@ -122,13 +146,7 @@ export function openDb(dbPath: string): LockedDatabase {
const dir = path.dirname(dbPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
acquireAdvisoryLock(dbPath);
// vendor.d.ts declares Database as a callable; cast through unknown for construct usage
const db = new (
Database as unknown as new (
path: string,
opts?: Record<string, unknown>,
) => LockedDatabase
)(dbPath);
const db = new Database(dbPath) as unknown as LockedDatabase;
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
db.__lockPath = `${dbPath}.lock`;
Expand Down Expand Up @@ -190,12 +208,33 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
{ file: dbPath },
);
}
return new (
const db = new (
Database as unknown as new (
path: string,
opts?: Record<string, unknown>,
) => BetterSqlite3Database
)(dbPath, { readonly: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inconsistent constructor cast left in openReadonlyOrFail

openDb was simplified to new Database(dbPath) as unknown as LockedDatabase, but openReadonlyOrFail still uses the verbose three-line new (Database as unknown as new (...) => BetterSqlite3Database)(...) form. Now that @types/better-sqlite3 is a declared dev-dependency and properly types Database as a class constructor, the cast can be reduced to the same two-step as unknown as pattern used elsewhere:

  const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Simplified openReadonlyOrFail to use the same new Database(...) as unknown as BetterSqlite3Database pattern as openDb. Commit: 9b469df.


// Warn once per process if the DB was built with a different codegraph version
if (!_versionWarned) {
try {
const row = db
.prepare<{ value: string }>('SELECT value FROM build_meta WHERE key = ?')
.get('codegraph_version');
const buildVersion = row?.value;
const currentVersion = getPackageVersion();
if (buildVersion && currentVersion && buildVersion !== currentVersion) {
warn(
`DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
);
}
} catch {
// build_meta table may not exist in older DBs — silently ignore
}
_versionWarned = true;
}

return db;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/domain/graph/builder/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* Each stage reads what it needs and writes what it produces.
* This replaces the closure-captured locals in the old monolithic buildGraph().
*/
import type BetterSqlite3 from 'better-sqlite3';
import type {
BetterSqlite3Database,
BuildGraphOpts,
CodegraphConfig,
EngineOpts,
Expand All @@ -20,7 +20,7 @@ import type {
export class PipelineContext {
// ── Inputs (set during setup) ──────────────────────────────────────
rootDir!: string;
db!: BetterSqlite3.Database;
db!: BetterSqlite3Database;
dbPath!: string;
config!: CodegraphConfig;
opts!: BuildGraphOpts;
Expand Down
26 changes: 15 additions & 11 deletions src/domain/graph/builder/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import type BetterSqlite3 from 'better-sqlite3';
import { purgeFilesData } from '../../../db/index.js';
import { warn } from '../../../infrastructure/logger.js';
import { debug, warn } from '../../../infrastructure/logger.js';
import { EXTENSIONS, IGNORE_DIRS } from '../../../shared/constants.js';
import type { BetterSqlite3Database, CodegraphConfig, PathAliases } from '../../../types.js';
import type {
BetterSqlite3Database,
CodegraphConfig,
PathAliases,
SqliteStatement,
} from '../../../types.js';

export const BUILTIN_RECEIVERS: Set<string> = new Set([
'console',
Expand Down Expand Up @@ -149,7 +153,7 @@ export function loadPathAliases(rootDir: string): PathAliases {
}
break;
} catch (err: unknown) {
warn(`Failed to parse ${configName}: ${(err as Error).message}`);
debug(`Failed to parse ${configName}: ${(err as Error).message}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 warn silently downgraded to debug for path-alias parse failures

The log level for a failed tsconfig.json / jsconfig.json parse was changed from warn to debug. This is a user-visible regression: if a consumer has a syntactically invalid config file, the failure is now completely silent in normal operation — only surfacing with --debug / LOG_LEVEL=debug. Users who rely on this warning to detect misconfigured path aliases will lose the signal entirely.

If the intent is to suppress noise when a project simply has no tsconfig (the path does not exist, so JSON.parse throws), a tighter fix would be to distinguish a parse error from a missing-file error and only demote the latter:

    } catch (err: unknown) {
      const msg = (err as Error).message;
      // Suppress "no such file" noise; surface genuine parse errors
      if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
        debug(`Config not found: ${configName}`);
      } else {
        warn(`Failed to parse ${configName}: ${msg}`);
      }
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The fs.existsSync(configPath) guard on line 135 already skips missing files before entering the try block, so the catch only fires on genuine parse errors. Restored warn level so users see malformed tsconfig/jsconfig errors in normal output. Commit: 9b469df.

}
}
return aliases;
Expand Down Expand Up @@ -199,7 +203,7 @@ export function readFileSafe(filePath: string, retries: number = 2): string {
* Purge all graph data for the specified files.
*/
export function purgeFilesFromGraph(
db: BetterSqlite3.Database,
db: BetterSqlite3Database,
files: string[],
options: Record<string, unknown> = {},
): void {
Expand All @@ -211,10 +215,10 @@ export function purgeFilesFromGraph(
const BATCH_CHUNK = 500;

// Statement caches keyed by chunk size — avoids recompiling for every batch.
const nodeStmtCache = new WeakMap<BetterSqlite3.Database, Map<number, BetterSqlite3.Statement>>();
const edgeStmtCache = new WeakMap<BetterSqlite3.Database, Map<number, BetterSqlite3.Statement>>();
const nodeStmtCache = new WeakMap<BetterSqlite3Database, Map<number, SqliteStatement>>();
const edgeStmtCache = new WeakMap<BetterSqlite3Database, Map<number, SqliteStatement>>();

function getNodeStmt(db: BetterSqlite3.Database, chunkSize: number): BetterSqlite3.Statement {
function getNodeStmt(db: BetterSqlite3Database, chunkSize: number): SqliteStatement {
let cache = nodeStmtCache.get(db);
if (!cache) {
cache = new Map();
Expand All @@ -232,7 +236,7 @@ function getNodeStmt(db: BetterSqlite3.Database, chunkSize: number): BetterSqlit
return stmt;
}

function getEdgeStmt(db: BetterSqlite3.Database, chunkSize: number): BetterSqlite3.Statement {
function getEdgeStmt(db: BetterSqlite3Database, chunkSize: number): SqliteStatement {
let cache = edgeStmtCache.get(db);
if (!cache) {
cache = new Map();
Expand All @@ -254,7 +258,7 @@ function getEdgeStmt(db: BetterSqlite3.Database, chunkSize: number): BetterSqlit
* Batch-insert node rows via multi-value INSERT statements.
* Each row: [name, kind, file, line, end_line, parent_id, qualified_name, scope, visibility]
*/
export function batchInsertNodes(db: BetterSqlite3.Database, rows: unknown[][]): void {
export function batchInsertNodes(db: BetterSqlite3Database, rows: unknown[][]): void {
if (!rows.length) return;
for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
const end = Math.min(i + BATCH_CHUNK, rows.length);
Expand All @@ -273,7 +277,7 @@ export function batchInsertNodes(db: BetterSqlite3.Database, rows: unknown[][]):
* Batch-insert edge rows via multi-value INSERT statements.
* Each row: [source_id, target_id, kind, confidence, dynamic]
*/
export function batchInsertEdges(db: BetterSqlite3.Database, rows: unknown[][]): void {
export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]): void {
if (!rows.length) return;
for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
const end = Math.min(i + BATCH_CHUNK, rows.length);
Expand Down
Loading
Loading