Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,58 @@

All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.

## [2.4.0](https://github.com/optave/codegraph/compare/v2.3.0...v2.4.0) (2026-02-25)

**Co-change analysis, node roles, faster parsing, and richer Mermaid output.** This release adds git co-change analysis to surface files that change together, classifies nodes by architectural role (entry/core/utility/adapter/dead/leaf), replaces the manual AST walk with tree-sitter's Query API for significantly faster JS/TS/TSX extraction, and enhances Mermaid export with subgraphs, edge labels, node shapes, and styling.

### Features

* **cli:** add git co-change analysis — surfaces files that frequently change together using Jaccard similarity on git history ([61785f7](https://github.com/optave/codegraph/commit/61785f7))
* **cli:** add node role classification — automatically labels nodes as entry, core, utility, adapter, dead, or leaf based on graph topology ([165f6ca](https://github.com/optave/codegraph/commit/165f6ca))
* **cli:** add `--json` to `search`, `--file` glob filter, `--exclude` to `prune`, exclude worktrees from vitest ([00ed205](https://github.com/optave/codegraph/commit/00ed205))
* **cli:** add update notification after commands — checks npm for newer versions and displays an upgrade hint ([eb3ccdf](https://github.com/optave/codegraph/commit/eb3ccdf))
* **export:** enhance Mermaid export with subgraphs, edge labels, node shapes, and styling ([ae301c0](https://github.com/optave/codegraph/commit/ae301c0))

### Performance

* **parser:** replace manual AST walk with tree-sitter Query API for JS/TS/TSX extraction ([fb6a139](https://github.com/optave/codegraph/commit/fb6a139))
* **builder:** avoid disk reads for line counts during incremental rebuild ([7b538bc](https://github.com/optave/codegraph/commit/7b538bc))

### Bug Fixes

* **builder:** preserve structure data during incremental builds ([7377fd9](https://github.com/optave/codegraph/commit/7377fd9))
* **embedder:** make embed command respect config `embeddings.model` ([77ffffc](https://github.com/optave/codegraph/commit/77ffffc))
* **embedder:** use `DEFAULT_MODEL` as single source of truth for embed default ([832fa49](https://github.com/optave/codegraph/commit/832fa49))
* **embedder:** add model disposal to prevent ONNX memory leak ([383e899](https://github.com/optave/codegraph/commit/383e899))
* **export:** escape quotes in Mermaid labels ([1c4ca34](https://github.com/optave/codegraph/commit/1c4ca34))
* **queries:** recompute Jaccard from total file counts during incremental co-change analysis ([e2a771b](https://github.com/optave/codegraph/commit/e2a771b))
* **queries:** collect all distinct edge kinds per pair instead of keeping only first ([4f40eee](https://github.com/optave/codegraph/commit/4f40eee))
* **queries:** skip keys without `::` separator in role lookup ([0c10e23](https://github.com/optave/codegraph/commit/0c10e23))
* **resolve:** use `indexOf` for `::` split to handle paths with colons ([b9d6ae4](https://github.com/optave/codegraph/commit/b9d6ae4))
* validate glob patterns and exclude names, clarify regex escaping ([6cf191f](https://github.com/optave/codegraph/commit/6cf191f))
* clean up regex escaping and remove unsupported brace from glob detection ([ab0d3a0](https://github.com/optave/codegraph/commit/ab0d3a0))
* **ci:** prevent benchmark updater from deleting README subsections ([bd1682a](https://github.com/optave/codegraph/commit/bd1682a))
* **ci:** add `--allow-same-version` to `npm version` in publish workflow ([9edaf15](https://github.com/optave/codegraph/commit/9edaf15))

### Refactoring

* reuse `coChangeForFiles` in `diffImpactData` ([aef1787](https://github.com/optave/codegraph/commit/aef1787))

### Testing

* add query vs walk parity tests for JS/TS/TSX extractors ([e68f6a7](https://github.com/optave/codegraph/commit/e68f6a7))

### Chores

* configure `bge-large` as default embedding model ([c21c387](https://github.com/optave/codegraph/commit/c21c387))

### Documentation

* add co-change analysis to README and mark backlog #9 done ([f977f9c](https://github.com/optave/codegraph/commit/f977f9c))
* reorganize docs — move guides to `docs/guides/`, roadmap into `docs/` ([ad423b7](https://github.com/optave/codegraph/commit/ad423b7))
* move roadmap files into `docs/roadmap/` ([693a8aa](https://github.com/optave/codegraph/commit/693a8aa))
* add Plan Mode Default working principle to CLAUDE.md ([c682f38](https://github.com/optave/codegraph/commit/c682f38))

## [2.3.0](https://github.com/optave/codegraph/compare/v2.2.1...v2.3.0) (2026-02-23)

**Smarter embeddings, richer CLI output, and robustness fixes.** This release introduces graph-enriched embedding strategies that use dependency context instead of raw source code, adds config-level test exclusion and recursive explain depth, outputs Mermaid diagrams from `diff-impact`, filters low-confidence edges from exports, and fixes numerous issues found through dogfooding.
Expand Down
38 changes: 12 additions & 26 deletions package-lock.json

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

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@optave/codegraph",
"version": "2.3.0",
"version": "2.4.0",
"description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
"type": "module",
"main": "src/index.js",
Expand Down Expand Up @@ -64,8 +64,7 @@
"@modelcontextprotocol/sdk": "^1.0.0",
"@optave/codegraph-darwin-arm64": "2.3.0",
"@optave/codegraph-darwin-x64": "2.3.0",
"@optave/codegraph-linux-x64-gnu": "2.3.0",
"@optave/codegraph-win32-x64-msvc": "2.3.0"
"@optave/codegraph-linux-x64-gnu": "2.3.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.4",
Expand Down
12 changes: 12 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
registerRepo,
unregisterRepo,
} from './registry.js';
import { checkForUpdates, printUpdateNotification } from './update-check.js';
import { watchProject } from './watcher.js';

const __cliDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
Expand All @@ -56,6 +57,17 @@ program
.hook('preAction', (thisCommand) => {
const opts = thisCommand.opts();
if (opts.verbose) setVerbose(true);
})
.hook('postAction', async (_thisCommand, actionCommand) => {
const name = actionCommand.name();
if (name === 'mcp' || name === 'watch') return;
if (actionCommand.opts().json) return;
try {
const result = await checkForUpdates(pkg.version);
if (result) printUpdateNotification(result.current, result.latest);
} catch {
/* never break CLI */
}
});

/**
Expand Down
159 changes: 159 additions & 0 deletions src/update-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import fs from 'node:fs';
import https from 'node:https';
import os from 'node:os';
import path from 'node:path';

const CACHE_PATH =
process.env.CODEGRAPH_UPDATE_CACHE_PATH ||
path.join(os.homedir(), '.codegraph', 'update-check.json');

const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
const FETCH_TIMEOUT_MS = 3000;
const REGISTRY_URL = 'https://registry.npmjs.org/@optave/codegraph/latest';

/**
* Minimal semver comparison. Returns -1, 0, or 1.
* Only handles numeric x.y.z (no pre-release tags).
*/
export function semverCompare(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;
if (na < nb) return -1;
if (na > nb) return 1;
}
return 0;
}

/**
* Load the cached update-check result from disk.
* Returns null on missing or corrupt file.
*/
function loadCache(cachePath = CACHE_PATH) {
try {
const raw = fs.readFileSync(cachePath, 'utf-8');
const data = JSON.parse(raw);
if (!data || typeof data.lastCheckedAt !== 'number' || typeof data.latestVersion !== 'string') {
return null;
}
return data;
} catch {
return null;
}
}

/**
* Persist the cache to disk (atomic write via temp + rename).
*/
function saveCache(cache, cachePath = CACHE_PATH) {
const dir = path.dirname(cachePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

const tmp = `${cachePath}.tmp.${process.pid}`;
fs.writeFileSync(tmp, JSON.stringify(cache), 'utf-8');
fs.renameSync(tmp, cachePath);
}

/**
* Fetch the latest version string from the npm registry.
* Returns the version string or null on failure.
*/
function fetchLatestVersion() {
return new Promise((resolve) => {
const req = https.get(
REGISTRY_URL,
{ timeout: FETCH_TIMEOUT_MS, headers: { Accept: 'application/json' } },
(res) => {
if (res.statusCode !== 200) {
res.resume();
resolve(null);
return;
}
let body = '';
res.setEncoding('utf-8');
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
try {
const data = JSON.parse(body);
resolve(typeof data.version === 'string' ? data.version : null);
} catch {
resolve(null);
}
});
},
);
req.on('error', () => resolve(null));
req.on('timeout', () => {
req.destroy();
resolve(null);
});
});
}

/**
* Check whether a newer version of codegraph is available.
*
* Returns `{ current, latest }` if an update is available, `null` otherwise.
* Silently returns null on any error — never affects CLI operation.
*
* Options:
* cachePath — override cache file location (for testing)
* _fetchLatest — override the fetch function (for testing)
*/
export async function checkForUpdates(currentVersion, options = {}) {
// Suppress in non-interactive / CI contexts
if (process.env.CI) return null;
if (process.env.NO_UPDATE_CHECK) return null;
if (!process.stderr.isTTY) return null;

const cachePath = options.cachePath || CACHE_PATH;
const fetchFn = options._fetchLatest || fetchLatestVersion;

try {
const cache = loadCache(cachePath);

// Cache is fresh — use it
if (cache && Date.now() - cache.lastCheckedAt < CACHE_TTL_MS) {
if (semverCompare(currentVersion, cache.latestVersion) < 0) {
return { current: currentVersion, latest: cache.latestVersion };
}
return null;
}

// Cache is stale or missing — fetch
const latest = await fetchFn();
if (!latest) return null;

// Update cache regardless of result
saveCache({ lastCheckedAt: Date.now(), latestVersion: latest }, cachePath);

if (semverCompare(currentVersion, latest) < 0) {
return { current: currentVersion, latest };
}
return null;
} catch {
return null;
}
}

/**
* Print a visible update notification box to stderr.
*/
export function printUpdateNotification(current, latest) {
const msg1 = `Update available: ${current} → ${latest}`;
const msg2 = 'Run `npm i -g @optave/codegraph` to update';
const width = Math.max(msg1.length, msg2.length) + 4;

const top = `┌${'─'.repeat(width)}┐`;
const bot = `└${'─'.repeat(width)}┘`;
const pad1 = ' '.repeat(width - msg1.length - 2);
const pad2 = ' '.repeat(width - msg2.length - 2);
const line1 = `│ ${msg1}${pad1}│`;
const line2 = `│ ${msg2}${pad2}│`;

process.stderr.write(`\n${top}\n${line1}\n${line2}\n${bot}\n\n`);
}
Loading