From 253e3a7a2e95a060b0d77326ca5cdeccbe4d9edd Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:28:02 -0600 Subject: [PATCH 1/4] fix: db version warning, barrel export tracing, quieter tsconfig, Set compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 9.1 — Warn on graph load when DB was built with a different codegraph version. The check runs once per process in openReadonlyOrFail() and suggests `build --no-incremental`. 9.2 — Barrel-only files now emit reexport edges during build. Previously the entire file was skipped in buildImportEdges; now only non-reexport imports are skipped, so `codegraph exports` can follow re-export chains. 9.3 — Demote "Failed to parse tsconfig.json" from warn to debug level so it no longer clutters every build output. 9.4 — Document EXTENSIONS/IGNORE_DIRS Array→Set breaking change in CHANGELOG. Add .toArray() convenience method and export ArrayCompatSet type for consumers migrating from the pre-3.4 array API. --- CHANGELOG.md | 4 ++ src/db/connection.ts | 47 +++++++++++++++- src/domain/graph/builder/helpers.ts | 4 +- .../graph/builder/stages/build-edges.ts | 5 +- src/index.ts | 1 + src/shared/constants.ts | 54 ++++++++++++------- 6 files changed, 92 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda874a1..bc2f60ac 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)) +### ⚠ BREAKING CHANGES (programmatic API) + +* **constants:** `EXTENSIONS` and `IGNORE_DIRS` are now `Set` instead of `Array`. Consumers using `.includes()`, `.indexOf()`, or array spread should migrate to `.has()` and `[...set]` respectively. Both sets expose a `.toArray()` convenience method for a quick migration path. + ## [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..2f743827 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 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`, + ); + _versionWarned = true; + } + } catch { + // build_meta table may not exist in older DBs — silently ignore + } + } + + return db; } /** diff --git a/src/domain/graph/builder/helpers.ts b/src/domain/graph/builder/helpers.ts index 15451b76..90ec25ac 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'; @@ -149,7 +149,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 70da56f4..cca14d33 100644 --- a/src/domain/graph/builder/stages/build-edges.ts +++ b/src/domain/graph/builder/stages/build-edges.ts @@ -102,12 +102,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; 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..46c56aff 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(SUPPORTED_EXTENSIONS); export function shouldIgnore(dirName: string): boolean { return IGNORE_DIRS.has(dirName) || dirName.startsWith('.'); From 8e43e43fb848bd108fd459b4739078a63cd37073 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:35:08 -0600 Subject: [PATCH 2/4] docs: soften EXTENSIONS/IGNORE_DIRS changelog wording --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2f60ac..f0220e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,9 +40,9 @@ 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)) -### ⚠ BREAKING CHANGES (programmatic API) +### Notes -* **constants:** `EXTENSIONS` and `IGNORE_DIRS` are now `Set` instead of `Array`. Consumers using `.includes()`, `.indexOf()`, or array spread should migrate to `.has()` and `[...set]` respectively. Both sets expose a `.toArray()` convenience method for a quick migration path. +* **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) From b60fbb77402de95175cccdff9cc2a2d0aa5c399d Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:52:39 -0600 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20version=20check,=20Set=20mutation,=20barrel=20edge?= =?UTF-8?q?=20duplication=20(#634)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move _versionWarned flag outside mismatch conditional to avoid redundant build_meta queries when versions match. - Wrap SUPPORTED_EXTENSIONS in new Set() to avoid mutating the sibling module's export. - Delete outgoing edges for barrel-only files before re-adding them to fileSymbols during incremental builds, preventing duplicate reexport edges. --- src/db/connection.ts | 4 ++-- src/domain/graph/builder/stages/resolve-imports.ts | 6 ++++++ src/shared/constants.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/db/connection.ts b/src/db/connection.ts index 2f743827..0c9760ca 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -221,7 +221,7 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database { ) => BetterSqlite3Database )(dbPath, { readonly: true }); - // Warn once if the DB was built with a different codegraph version + // Warn once per process if the DB was built with a different codegraph version if (!_versionWarned) { try { const row = db @@ -233,11 +233,11 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database { warn( `DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`, ); - _versionWarned = true; } } catch { // build_meta table may not exist in older DBs — silently ignore } + _versionWarned = true; } return db; diff --git a/src/domain/graph/builder/stages/resolve-imports.ts b/src/domain/graph/builder/stages/resolve-imports.ts index eb828386..fe5df217 100644 --- a/src/domain/graph/builder/stages/resolve-imports.ts +++ b/src/domain/graph/builder/stages/resolve-imports.ts @@ -46,6 +46,11 @@ export async function resolveImports(ctx: PipelineContext): Promise { JOIN nodes n1 ON e.source_id = n1.id WHERE e.kind = 'reexports' AND n1.kind = 'file'`) .all() as Array<{ file: string }>; + // Barrel-only files will have edges re-created by buildEdges; delete + // their outgoing edges first to prevent duplicates during incremental builds. + const deleteOutgoingEdges = db.prepare( + 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)', + ); for (const { file: relPath } of barrelCandidates) { if (fileSymbols.has(relPath)) continue; const absPath = path.join(rootDir, relPath); @@ -53,6 +58,7 @@ export async function resolveImports(ctx: PipelineContext): Promise { const symbols = await parseFilesAuto([absPath], rootDir, engineOpts); const fileSym = symbols.get(relPath); if (fileSym) { + deleteOutgoingEdges.run(relPath); fileSymbols.set(relPath, fileSym); ctx.barrelOnlyFiles.add(relPath); const reexports = fileSym.imports.filter((imp: Import) => imp.reexport); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 46c56aff..1b20e4cd 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -36,7 +36,7 @@ export const IGNORE_DIRS: ArrayCompatSet = withArrayCompat( ]), ); -export const EXTENSIONS: ArrayCompatSet = withArrayCompat(SUPPORTED_EXTENSIONS); +export const EXTENSIONS: ArrayCompatSet = withArrayCompat(new Set(SUPPORTED_EXTENSIONS)); export function shouldIgnore(dirName: string): boolean { return IGNORE_DIRS.has(dirName) || dirName.startsWith('.'); From 74453fde73f4fd7eeeeb36e83aae1ea2a52ac98e Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:27:46 -0600 Subject: [PATCH 4/4] fix: move barrel edge deletion into buildEdges transaction for atomicity (#634) --- src/domain/graph/builder/stages/build-edges.ts | 11 +++++++++++ src/domain/graph/builder/stages/resolve-imports.ts | 6 ------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/domain/graph/builder/stages/build-edges.ts b/src/domain/graph/builder/stages/build-edges.ts index cca14d33..0706489e 100644 --- a/src/domain/graph/builder/stages/build-edges.ts +++ b/src/domain/graph/builder/stages/build-edges.ts @@ -576,6 +576,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/domain/graph/builder/stages/resolve-imports.ts b/src/domain/graph/builder/stages/resolve-imports.ts index fe5df217..eb828386 100644 --- a/src/domain/graph/builder/stages/resolve-imports.ts +++ b/src/domain/graph/builder/stages/resolve-imports.ts @@ -46,11 +46,6 @@ export async function resolveImports(ctx: PipelineContext): Promise { JOIN nodes n1 ON e.source_id = n1.id WHERE e.kind = 'reexports' AND n1.kind = 'file'`) .all() as Array<{ file: string }>; - // Barrel-only files will have edges re-created by buildEdges; delete - // their outgoing edges first to prevent duplicates during incremental builds. - const deleteOutgoingEdges = db.prepare( - 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)', - ); for (const { file: relPath } of barrelCandidates) { if (fileSymbols.has(relPath)) continue; const absPath = path.join(rootDir, relPath); @@ -58,7 +53,6 @@ export async function resolveImports(ctx: PipelineContext): Promise { const symbols = await parseFilesAuto([absPath], rootDir, engineOpts); const fileSym = symbols.get(relPath); if (fileSym) { - deleteOutgoingEdges.run(relPath); fileSymbols.set(relPath, fileSym); ctx.barrelOnlyFiles.add(relPath); const reexports = fileSym.imports.filter((imp: Import) => imp.reexport);