Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]

### Added
- **`codegraph status --json` now reports the CLI version and a real last-indexed timestamp for CI/scripting (#329).** Three new fields are surfaced in the JSON output that previously only existed (when at all) in the human-readable form:
- `codegraphVersion` — the running CLI's version (matches `package.json`), included in both the initialized and not-initialized branches so CI can pin against a known version without an extra `codegraph --version` call.
- `lastIndexedAt` — ms-since-epoch of the most recent file `indexed_at` write, computed via `MAX(indexed_at)` over the `files` table (one indexed query, no per-file scan). `null` when the project has no tracked files.
- `lastIndexedAtIso` — the same instant in ISO-8601, for log/human consumers; mirrors `lastIndexedAt` exactly (or `null`).

Surfaces the same value via a new public API method, `CodeGraph.getLastIndexedAt(): number | null`, for library consumers that want index-freshness checks without shelling out to the CLI. Closes #329.
- **Java / Kotlin imports now resolve by fully-qualified name.** Extraction
wraps every top-level declaration of a `.kt` / `.java` file in a `namespace`
node carrying the file's `package` (so a class `Bar` in
Expand Down
113 changes: 113 additions & 0 deletions __tests__/status-json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Tests for the fields `codegraph status --json` exposes for CI/scripting
* consumers (issue #329) — specifically the `codegraphVersion` field and the
* `lastIndexedAt` / `lastIndexedAtIso` pair populated from `MAX(indexed_at)`
* over the `files` table.
*
* The CLI itself is exercised end-to-end so the JSON shape and field names
* survive future refactors of the underlying `getLastIndexedAt()` plumbing.
*/

import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
const PKG_VERSION = JSON.parse(
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'),
).version as string;

function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-status-json-'));
}

function runStatusJson(cwd: string): Record<string, unknown> {
const stdout = execFileSync(process.execPath, [BIN, 'status', '--json'], {
cwd,
encoding: 'utf-8',
env: { ...process.env, CODEGRAPH_ALLOW_UNSAFE_NODE: '1' },
stdio: ['ignore', 'pipe', 'ignore'],
});
return JSON.parse(stdout.trim());
}

describe('CodeGraph.getLastIndexedAt()', () => {
let tempDir: string;

beforeEach(() => { tempDir = createTempDir(); });
afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); });

it('returns null on a fresh project with no indexed files', () => {
const cg = CodeGraph.initSync(tempDir);
expect(cg.getLastIndexedAt()).toBeNull();
cg.close();
});

it('returns the most recent file `indexed_at` after indexing', async () => {
fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
fs.writeFileSync(path.join(tempDir, 'b.ts'), 'export const y = 2;\n');

const before = Date.now();
const cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
const after = Date.now();

const last = cg.getLastIndexedAt();
expect(last).not.toBeNull();
expect(typeof last).toBe('number');
// Bracketed by the wall-clock window the test spent in indexAll().
expect(last!).toBeGreaterThanOrEqual(before);
expect(last!).toBeLessThanOrEqual(after);

cg.close();
});
});

describe('codegraph status --json fields (issue #329)', () => {
let tempDir: string;

// Built binary is required for the CLI invocations below. `npm test` builds
// dist/ once via the implicit `build` script that other tests already depend
// on; here we just verify the entry point exists so a friendlier error fires
// if someone runs this test in isolation without building first.
beforeAll(() => {
if (!fs.existsSync(BIN)) {
throw new Error(`dist/ not built — run \`npm run build\` before this test. Missing: ${BIN}`);
}
});

beforeEach(() => { tempDir = createTempDir(); });
afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); });

it('emits codegraphVersion even when the project is not initialized', () => {
const out = runStatusJson(tempDir);
expect(out.initialized).toBe(false);
expect(out.codegraphVersion).toBe(PKG_VERSION);
expect(out.projectPath).toBe(fs.realpathSync(tempDir));
});

it('emits codegraphVersion, lastIndexedAt (ms), and lastIndexedAtIso once indexed', async () => {
fs.writeFileSync(path.join(tempDir, 'main.ts'), 'export function hello() { return 1; }\n');
const cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.close();

const out = runStatusJson(tempDir);
expect(out.initialized).toBe(true);
expect(out.codegraphVersion).toBe(PKG_VERSION);

// Numeric ms-since-epoch (the CI-friendly form), within a reasonable
// window around the test's wall clock.
expect(typeof out.lastIndexedAt).toBe('number');
const last = out.lastIndexedAt as number;
expect(last).toBeGreaterThan(Date.now() - 5 * 60_000);
expect(last).toBeLessThanOrEqual(Date.now());

// ISO mirror of the same instant, for human/log consumers.
expect(typeof out.lastIndexedAtIso).toBe('string');
expect(new Date(out.lastIndexedAtIso as string).getTime()).toBe(last);
});
});
13 changes: 12 additions & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,11 @@ program
try {
if (!isInitialized(projectPath)) {
if (options.json) {
console.log(JSON.stringify({ initialized: false, projectPath }));
console.log(JSON.stringify({
codegraphVersion: packageJson.version,
initialized: false,
projectPath,
}));
return;
}
console.log(chalk.bold('\nCodeGraph Status\n'));
Expand All @@ -721,7 +725,9 @@ program

// JSON output mode
if (options.json) {
const lastIndexedAt = cg.getLastIndexedAt();
console.log(JSON.stringify({
codegraphVersion: packageJson.version,
initialized: true,
projectPath,
fileCount: stats.fileCount,
Expand All @@ -732,6 +738,11 @@ program
journalMode,
nodesByKind: stats.nodesByKind,
languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
// ms-since-epoch of the most recent file `indexed_at` write, or null
// when no files are tracked. CI can pair this with the ISO field
// below or compare against `Date.now()` for staleness checks.
lastIndexedAt,
lastIndexedAtIso: lastIndexedAt !== null ? new Date(lastIndexedAt).toISOString() : null,
pendingChanges: {
added: changes.added.length,
modified: changes.modified.length,
Expand Down
13 changes: 13 additions & 0 deletions src/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,19 @@ export class QueryBuilder {
};
}

/**
* Wall-clock ms (UTC) of the most recent file `indexed_at` write, or `null`
* when no files are tracked. Used by `codegraph status --json` (and any
* library consumer) to assert index freshness without scanning every file
* row — see issue #329.
*/
getLastIndexedAt(): number | null {
const row = this.db
.prepare('SELECT MAX(indexed_at) AS last FROM files')
.get() as { last: number | null } | undefined;
return row?.last ?? null;
}

// ===========================================================================
// Project Metadata
// ===========================================================================
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,16 @@ export class CodeGraph {
return stats;
}

/**
* Wall-clock ms (UTC) of the most recent file `indexed_at` write, or `null`
* when the project has no tracked files. Surfaced as `lastIndexedAt` by
* `codegraph status --json` so CI scripts can assert index freshness without
* shelling out to SQL — see issue #329.
*/
getLastIndexedAt(): number | null {
return this.queries.getLastIndexedAt();
}

/**
* Active SQLite backend for this project's connection (`node-sqlite` — Node's
* built-in real-SQLite module). Surfaced via `codegraph status` and the
Expand Down