diff --git a/CHANGELOG.md b/CHANGELOG.md index dda874a1..f0220e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` (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. diff --git a/src/db/connection.ts b/src/db/connection.ts index cadd04e0..0c9760ca 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -1,6 +1,7 @@ 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'; @@ -8,6 +9,24 @@ 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 }; @@ -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); @@ -190,12 +214,33 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database { { file: dbPath }, ); } - return new ( + const db = new ( Database as unknown as new ( path: string, opts?: Record, ) => BetterSqlite3Database )(dbPath, { readonly: true }); + + // 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; } /** diff --git a/src/domain/graph/builder/helpers.ts b/src/domain/graph/builder/helpers.ts index 3d1be287..3b5208df 100644 --- a/src/domain/graph/builder/helpers.ts +++ b/src/domain/graph/builder/helpers.ts @@ -8,7 +8,7 @@ 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'; @@ -148,7 +148,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}`); } } return aliases; diff --git a/src/domain/graph/builder/stages/build-edges.ts b/src/domain/graph/builder/stages/build-edges.ts index a5878a1b..f7ad1d36 100644 --- a/src/domain/graph/builder/stages/build-edges.ts +++ b/src/domain/graph/builder/stages/build-edges.ts @@ -101,12 +101,15 @@ function buildImportEdges( const { fileSymbols, barrelOnlyFiles, rootDir } = ctx; for (const [relPath, symbols] of fileSymbols) { - if (barrelOnlyFiles.has(relPath)) continue; + const isBarrelOnly = barrelOnlyFiles.has(relPath); const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0); if (!fileNodeRow) continue; const fileNodeId = fileNodeRow.id; for (const imp of symbols.imports) { + // Barrel-only files: only emit reexport edges, skip regular imports + if (isBarrelOnly && !imp.reexport) continue; + const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source); const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0); if (!targetRow) continue; @@ -572,6 +575,17 @@ export async function buildEdges(ctx: PipelineContext): Promise { const t0 = performance.now(); const buildEdgesTx = db.transaction(() => { + // Delete stale outgoing edges for barrel-only files inside the transaction + // so that deletion and re-creation are atomic (no edge loss on mid-build crash). + if (ctx.barrelOnlyFiles.size > 0) { + const deleteOutgoingEdges = db.prepare( + 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)', + ); + for (const relPath of ctx.barrelOnlyFiles) { + deleteOutgoingEdges.run(relPath); + } + } + const allEdgeRows: EdgeRowTuple[] = []; buildImportEdges(ctx, getNodeIdStmt, allEdgeRows); diff --git a/src/index.ts b/src/index.ts index 99bb4aea..cfed7f65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,7 @@ export { sequenceData } from './features/sequence.js'; export { hotspotsData, moduleBoundariesData, structureData } from './features/structure.js'; export { triageData } from './features/triage.js'; export { loadConfig } from './infrastructure/config.js'; +export type { ArrayCompatSet } from './shared/constants.js'; export { EXTENSIONS, IGNORE_DIRS } from './shared/constants.js'; export { AnalysisError, diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 832bf001..1b20e4cd 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,26 +1,42 @@ import path from 'node:path'; import { SUPPORTED_EXTENSIONS } from '../domain/parser.js'; -export const IGNORE_DIRS: Set = new Set([ - 'node_modules', - '.git', - 'dist', - 'build', - '.next', - '.nuxt', - '.svelte-kit', - 'coverage', - '.codegraph', - '__pycache__', - '.tox', - 'vendor', - '.venv', - 'venv', - 'env', - '.env', -]); +/** + * Set with a `.toArray()` convenience method for consumers migrating from + * the pre-3.4 Array-based API (where `.includes()` / `.indexOf()` worked). + */ +export interface ArrayCompatSet extends Set { + toArray(): T[]; +} + +function withArrayCompat(s: Set): ArrayCompatSet { + const compat = s as ArrayCompatSet; + compat.toArray = () => [...s]; + return compat; +} + +export const IGNORE_DIRS: ArrayCompatSet = withArrayCompat( + new Set([ + 'node_modules', + '.git', + 'dist', + 'build', + '.next', + '.nuxt', + '.svelte-kit', + 'coverage', + '.codegraph', + '__pycache__', + '.tox', + 'vendor', + '.venv', + 'venv', + 'env', + '.env', + ]), +); -export { SUPPORTED_EXTENSIONS as EXTENSIONS }; +export const EXTENSIONS: ArrayCompatSet = withArrayCompat(new Set(SUPPORTED_EXTENSIONS)); export function shouldIgnore(dirName: string): boolean { return IGNORE_DIRS.has(dirName) || dirName.startsWith('.');