Skip to content

Commit f65b364

Browse files
feat: add build metadata tracking and excludeTests config shorthand
Track engine name, version, and codegraph version in a new build_meta table (migration v7). Warn during incremental builds when the engine or codegraph version has changed since the last build, suggesting --no-incremental. Display build metadata in the info command with mismatch warnings. Accept excludeTests at the top level of .codegraphrc.json as a shorthand for query.excludeTests, with the nested form taking precedence. Impact: 4 functions changed, 8 affected
1 parent 1aeea34 commit f65b364

8 files changed

Lines changed: 197 additions & 5 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,10 +532,15 @@ Create a `.codegraphrc.json` in your project root to customize behavior:
532532
},
533533
"build": {
534534
"incremental": true
535+
},
536+
"query": {
537+
"excludeTests": true
535538
}
536539
}
537540
```
538541

542+
> **Tip:** `excludeTests` can also be set at the top level as a shorthand — `{ "excludeTests": true }` is equivalent to nesting it under `query`. If both are present, the nested `query.excludeTests` takes precedence.
543+
539544
### LLM credentials
540545

541546
Codegraph supports an `apiKeyCommand` field for secure credential management. Instead of storing API keys in config files or environment variables, you can shell out to a secret manager at runtime:

src/builder.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import fs from 'node:fs';
33
import path from 'node:path';
44
import { loadConfig } from './config.js';
55
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
6-
import { initSchema, openDb } from './db.js';
6+
import { getBuildMeta, initSchema, openDb, setBuildMeta } from './db.js';
77
import { readJournal, writeJournalHeader } from './journal.js';
88
import { debug, info, warn } from './logger.js';
99
import { getActiveEngine, parseFilesAuto } from './parser.js';
1010
import { computeConfidence, resolveImportPath, resolveImportsBatch } from './resolve.js';
1111

1212
export { resolveImportPath } from './resolve.js';
1313

14+
const __builderDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
15+
const CODEGRAPH_VERSION = JSON.parse(
16+
fs.readFileSync(path.join(__builderDir, '..', 'package.json'), 'utf-8'),
17+
).version;
18+
1419
const BUILTIN_RECEIVERS = new Set([
1520
'console',
1621
'Math',
@@ -346,6 +351,22 @@ export async function buildGraph(rootDir, opts = {}) {
346351
const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
347352
info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
348353

354+
// Check for engine/version mismatch on incremental builds
355+
if (incremental) {
356+
const prevEngine = getBuildMeta(db, 'engine');
357+
const prevVersion = getBuildMeta(db, 'codegraph_version');
358+
if (prevEngine && prevEngine !== engineName) {
359+
warn(
360+
`Engine changed (${prevEngine}${engineName}). Consider rebuilding with --no-incremental for consistency.`,
361+
);
362+
}
363+
if (prevVersion && prevVersion !== CODEGRAPH_VERSION) {
364+
warn(
365+
`Codegraph version changed (${prevVersion}${CODEGRAPH_VERSION}). Consider rebuilding with --no-incremental for consistency.`,
366+
);
367+
}
368+
}
369+
349370
const aliases = loadPathAliases(rootDir);
350371
// Merge config aliases
351372
if (config.aliases) {
@@ -925,6 +946,18 @@ export async function buildGraph(rootDir, opts = {}) {
925946
}
926947
}
927948

949+
// Persist build metadata for mismatch detection
950+
try {
951+
setBuildMeta(db, {
952+
engine: engineName,
953+
engine_version: engineVersion || '',
954+
codegraph_version: CODEGRAPH_VERSION,
955+
built_at: new Date().toISOString(),
956+
});
957+
} catch (err) {
958+
debug(`Failed to write build metadata: ${err.message}`);
959+
}
960+
928961
db.close();
929962

930963
// Write journal header after successful build

src/cli.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,43 @@ program
668668
console.log(` Engine flag : --engine ${engine}`);
669669
console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
670670
console.log();
671+
672+
// Build metadata from DB
673+
try {
674+
const { findDbPath, getBuildMeta } = await import('./db.js');
675+
const Database = (await import('better-sqlite3')).default;
676+
const dbPath = findDbPath();
677+
const fs = await import('node:fs');
678+
if (fs.existsSync(dbPath)) {
679+
const db = new Database(dbPath, { readonly: true });
680+
const buildEngine = getBuildMeta(db, 'engine');
681+
const buildVersion = getBuildMeta(db, 'codegraph_version');
682+
const builtAt = getBuildMeta(db, 'built_at');
683+
db.close();
684+
685+
if (buildEngine || buildVersion || builtAt) {
686+
console.log('Build metadata');
687+
console.log('──────────────');
688+
if (buildEngine) console.log(` Engine : ${buildEngine}`);
689+
if (buildVersion) console.log(` Version : ${buildVersion}`);
690+
if (builtAt) console.log(` Built at : ${builtAt}`);
691+
692+
if (buildVersion && buildVersion !== program.version()) {
693+
console.log(
694+
` ⚠ DB was built with v${buildVersion}, current is v${program.version()}. Consider: codegraph build --no-incremental`,
695+
);
696+
}
697+
if (buildEngine && buildEngine !== activeName) {
698+
console.log(
699+
` ⚠ DB was built with ${buildEngine} engine, active is ${activeName}. Consider: codegraph build --no-incremental`,
700+
);
701+
}
702+
console.log();
703+
}
704+
}
705+
} catch {
706+
/* diagnostics must never crash */
707+
}
671708
});
672709

673710
program.parse();

src/config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ export function loadConfig(cwd) {
4545
const raw = fs.readFileSync(filePath, 'utf-8');
4646
const config = JSON.parse(raw);
4747
debug(`Loaded config from ${filePath}`);
48-
return resolveSecrets(applyEnvOverrides(mergeConfig(DEFAULTS, config)));
48+
const merged = mergeConfig(DEFAULTS, config);
49+
if ('excludeTests' in config && !(config.query && 'excludeTests' in config.query)) {
50+
merged.query.excludeTests = Boolean(config.excludeTests);
51+
}
52+
delete merged.excludeTests;
53+
return resolveSecrets(applyEnvOverrides(merged));
4954
} catch (err) {
5055
debug(`Failed to parse config ${filePath}: ${err.message}`);
5156
}

src/db.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,36 @@ export const MIGRATIONS = [
101101
);
102102
`,
103103
},
104+
{
105+
version: 7,
106+
up: `
107+
CREATE TABLE IF NOT EXISTS build_meta (
108+
key TEXT PRIMARY KEY,
109+
value TEXT NOT NULL
110+
);
111+
`,
112+
},
104113
];
105114

115+
export function getBuildMeta(db, key) {
116+
try {
117+
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
118+
return row ? row.value : null;
119+
} catch {
120+
return null;
121+
}
122+
}
123+
124+
export function setBuildMeta(db, entries) {
125+
const upsert = db.prepare('INSERT OR REPLACE INTO build_meta (key, value) VALUES (?, ?)');
126+
const tx = db.transaction(() => {
127+
for (const [key, value] of Object.entries(entries)) {
128+
upsert.run(key, String(value));
129+
}
130+
});
131+
tx();
132+
}
133+
106134
export function openDb(dbPath) {
107135
const dir = path.dirname(dbPath);
108136
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

src/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ export { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
2323
// Circular dependency detection
2424
export { findCycles, formatCycles } from './cycles.js';
2525
// Database utilities
26-
export { findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
26+
export {
27+
findDbPath,
28+
getBuildMeta,
29+
initSchema,
30+
openDb,
31+
openReadonlyOrFail,
32+
setBuildMeta,
33+
} from './db.js';
2734

2835
// Embeddings
2936
export {

tests/unit/config.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,34 @@ describe('loadConfig', () => {
162162
});
163163
});
164164

165+
describe('excludeTests hoisting', () => {
166+
it('hoists top-level excludeTests into query.excludeTests', () => {
167+
const dir = fs.mkdtempSync(path.join(tmpDir, 'exclude-top-'));
168+
fs.writeFileSync(path.join(dir, '.codegraphrc.json'), JSON.stringify({ excludeTests: true }));
169+
const config = loadConfig(dir);
170+
expect(config.query.excludeTests).toBe(true);
171+
expect(config.excludeTests).toBeUndefined();
172+
});
173+
174+
it('nested query.excludeTests takes precedence over top-level', () => {
175+
const dir = fs.mkdtempSync(path.join(tmpDir, 'exclude-nested-'));
176+
fs.writeFileSync(
177+
path.join(dir, '.codegraphrc.json'),
178+
JSON.stringify({ excludeTests: true, query: { excludeTests: false } }),
179+
);
180+
const config = loadConfig(dir);
181+
expect(config.query.excludeTests).toBe(false);
182+
});
183+
184+
it('hoists top-level excludeTests: false correctly', () => {
185+
const dir = fs.mkdtempSync(path.join(tmpDir, 'exclude-false-'));
186+
fs.writeFileSync(path.join(dir, '.codegraphrc.json'), JSON.stringify({ excludeTests: false }));
187+
const config = loadConfig(dir);
188+
expect(config.query.excludeTests).toBe(false);
189+
expect(config.excludeTests).toBeUndefined();
190+
});
191+
});
192+
165193
describe('applyEnvOverrides', () => {
166194
const ENV_KEYS = ['CODEGRAPH_LLM_PROVIDER', 'CODEGRAPH_LLM_API_KEY', 'CODEGRAPH_LLM_MODEL'];
167195

tests/unit/db.test.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
/**
2-
* Unit tests for src/db.js
2+
* Unit tests for src/db.js — build_meta helpers included
33
*/
44

55
import fs from 'node:fs';
66
import os from 'node:os';
77
import path from 'node:path';
88
import Database from 'better-sqlite3';
99
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
10-
import { findDbPath, initSchema, MIGRATIONS, openDb, openReadonlyOrFail } from '../../src/db.js';
10+
import {
11+
findDbPath,
12+
getBuildMeta,
13+
initSchema,
14+
MIGRATIONS,
15+
openDb,
16+
openReadonlyOrFail,
17+
setBuildMeta,
18+
} from '../../src/db.js';
1119

1220
let tmpDir;
1321

@@ -126,6 +134,47 @@ describe('findDbPath', () => {
126134
});
127135
});
128136

137+
describe('build_meta', () => {
138+
it('table is created by migration v7', () => {
139+
const db = new Database(':memory:');
140+
initSchema(db);
141+
const tables = db
142+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
143+
.all()
144+
.map((r) => r.name);
145+
expect(tables).toContain('build_meta');
146+
db.close();
147+
});
148+
149+
it('getBuildMeta returns null for missing table (pre-v7 schema)', () => {
150+
const db = new Database(':memory:');
151+
// No initSchema — no build_meta table
152+
const result = getBuildMeta(db, 'engine');
153+
expect(result).toBeNull();
154+
db.close();
155+
});
156+
157+
it('setBuildMeta writes and getBuildMeta reads', () => {
158+
const db = new Database(':memory:');
159+
initSchema(db);
160+
setBuildMeta(db, { engine: 'wasm', codegraph_version: '1.0.0' });
161+
expect(getBuildMeta(db, 'engine')).toBe('wasm');
162+
expect(getBuildMeta(db, 'codegraph_version')).toBe('1.0.0');
163+
expect(getBuildMeta(db, 'nonexistent')).toBeNull();
164+
db.close();
165+
});
166+
167+
it('setBuildMeta upserts existing keys', () => {
168+
const db = new Database(':memory:');
169+
initSchema(db);
170+
setBuildMeta(db, { engine: 'wasm' });
171+
expect(getBuildMeta(db, 'engine')).toBe('wasm');
172+
setBuildMeta(db, { engine: 'native' });
173+
expect(getBuildMeta(db, 'engine')).toBe('native');
174+
db.close();
175+
});
176+
});
177+
129178
describe('openReadonlyOrFail', () => {
130179
it('exits with error when DB does not exist', () => {
131180
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {

0 commit comments

Comments
 (0)