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

### New Features

- `codegraph_explore` finds code containing storage keys, command-line flags, and event names; rebuild existing indexes to enable these matches.

- **Codex and Astra read project guidance from `AGENTS.md`.** The canonical agent guide now lives in `AGENTS.md` (with a nested `docs/AGENTS.md` for long validation notes); `CLAUDE.md` is a thin `@AGENTS.md` wrapper for Claude Code. Codex/Astra no longer miss the old CLAUDE-only instructions.

- **Next.js pages and their navigation are in the graph.** App Router pages (`app/(group)/blog/[slug]/page.tsx` → `/blog/:slug`) and Pages Router pages are routes bound to the component they export, and `<Link href>`, an internal `<a href>`, `router.push` / `router.replace` (`next/navigation` and `next/router`), `redirect()` / `permanentRedirect()` and the middleware's `NextResponse.redirect(…)` are `navigates` edges between them. `app/api/**/route.ts` exports (`GET`, `POST`, …) are endpoints bound to their functions, and `pages/api/*` handlers are `ANY /api/…`. Re-index after upgrading.
Expand Down
3 changes: 2 additions & 1 deletion __tests__/kernel-retry-materialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe.skipIf(!kernelBuilt)('kernel buffer-transport storage (#1541)', () => {
it('storeExtractionResult persists the decoded nodes of a raw kernel result', async () => {
const source =
'def target_fn(root, mission_path):\n' +
' return (root, mission_path)\n' +
' return (root, mission_path, "adapter.mission")\n' +
'\n' +
'class Adapter:\n' +
' def adapt(self):\n' +
Expand Down Expand Up @@ -93,6 +93,7 @@ describe.skipIf(!kernelBuilt)('kernel buffer-transport storage (#1541)', () => {
expect(nodes.length).toBe(raw!.counts.nodes);
expect(nodes.map((n) => n.name)).toContain('target_fn');
expect(nodes.map((n) => n.name)).toContain('Adapter');
expect(cg.findLiteralSeedIds('"adapter.mission"').map(id => cg.getNode(id)?.name)).toEqual(['target_fn']);
});
});

Expand Down
78 changes: 78 additions & 0 deletions __tests__/literal-capture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect } from 'vitest';
import type { Node } from '../src/types';
import { captureLiterals, isSeedLiteral, seedLiteralsInQuery } from '../src/extraction/literal-capture';

function node(id: string, kind: Node['kind'], startLine: number, endLine: number): Node {
return {
id, kind, name: id, qualifiedName: id, filePath: 'src/a.ts', language: 'typescript',
startLine, endLine, startColumn: 0, endColumn: Number.MAX_SAFE_INTEGER, updatedAt: 0,
};
}

describe('isSeedLiteral', () => {
it('keeps storage keys, flags, dotted names and paths', () => {
for (const v of ['bompus_custom_ds_players', '--start', '-v', 'draft.pick', 'api/v1/users', 'ns:event'])
expect(isSeedLiteral(v), v).toBe(v !== '-v');
});
it('drops plain words, prose, and values that start with a separator', () => {
for (const v of ['ready', 'Error', 'not found', './utils', '../x', '', 'a_b'])
expect(isSeedLiteral(v), v).toBe(false);
});
});

describe('seedLiteralsInQuery', () => {
it('finds quoted spans and bare runs, stripping surrounding punctuation', () => {
expect(seedLiteralsInQuery('who writes "bompus_custom_ds_players" (via --start)?'))
.toEqual(['bompus_custom_ds_players', '--start']);
});
it('returns nothing for a symbol-anchored question', () => {
expect(seedLiteralsInQuery('callers of espnPlayerKey in shared')).toEqual([]);
});
});

describe('captureLiterals', () => {
const source = [
`import { x } from './utils/helpers';`, // 1: path starts with '.', never qualifies
`const KEY = 'bompus_custom_ds_players';`, // 2: top level → file node
`export function save() {`, // 3
` storage.set('bompus_custom_ds_players', 1);`, // 4
` log('ready');`, // 5: plain word
` emit(\`draft.pick\`); emit(\`draft.\${n}\`);`, // 6: second one interpolates
`}`, // 7
`export class Boot { start() { run('--start'); } }`,// 8
].join('\n');

it('attributes each literal to the innermost enclosing symbol, else the file', () => {
const file = node('file:src/a.ts', 'file', 1, 8);
const save = node('save', 'function', 3, 7);
const boot = node('Boot', 'class', 8, 8);
const start = node('start', 'method', 8, 8);
const imp = node('./utils/helpers', 'import', 1, 1);
const nodes = [file, imp, save, boot, start];
captureLiterals(source, nodes);
expect(file.literals).toEqual(['bompus_custom_ds_players']);
expect(save.literals).toEqual(['bompus_custom_ds_players', 'draft.pick']);
expect(start.literals).toEqual(['--start']);
expect(boot.literals).toBeUndefined();
expect(imp.literals).toBeUndefined();
});

it('dedupes per node and caps at 32', () => {
const many = Array.from({ length: 40 }, (_, i) => `k('key_${i}'); k('key_${i}');`).join('\n');
const fn = node('f', 'function', 1, 40);
captureLiterals(many, [node('file:src/a.ts', 'file', 1, 40), fn]);
expect(fn.literals).toHaveLength(32);
expect(new Set(fn.literals).size).toBe(32);
});

it('uses UTF-16 columns to distinguish same-line siblings after non-ASCII source', () => {
const prefix = '/* café 😀 */ ';
const first = "function writer(){return 'cache.write';}";
const second = "function reader(){return 'cache.read';}";
const writer = { ...node('writer', 'function', 1, 1), startColumn: prefix.length, endColumn: (prefix + first).length };
const reader = { ...node('reader', 'function', 1, 1), startColumn: (prefix + first).length, endColumn: (prefix + first + second).length };
captureLiterals(prefix + first + second, [writer, reader]);
expect(writer.literals).toEqual(['cache.write']);
expect(reader.literals).toEqual(['cache.read']);
});
});
59 changes: 59 additions & 0 deletions __tests__/literal-compiled-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import type CodeGraph from '../src';
import type { QueryBuilder } from '../src/db/queries';

const built = path.join(__dirname, '..', 'dist', 'index.js');
const kernel = path.join(__dirname, '..', 'codegraph-kernel', 'prebuilds', `${process.platform}-${process.arch}`, 'codegraph-kernel.node');

// Source-mode suites cannot exercise the compiled parse/store worker boundary.
describe.runIf(fs.existsSync(built))('literal persistence through compiled indexing', () => {
let dir: string | undefined;
let cg: CodeGraph | undefined;
afterEach(() => {
cg?.destroy();
cg = undefined;
if (dir) fs.rmSync(dir, { recursive: true, force: true });
dir = undefined;
vi.unstubAllEnvs();
});

for (const mode of ['native-worker', 'native-main', 'wasm-worker']) {
it.runIf(mode === 'wasm-worker' || fs.existsSync(kernel))(`${mode}: fresh, unchanged, and migrated indexes retain exact owners`, async () => {
vi.stubEnv('CODEGRAPH_PARSE_WORKERS', '1');
vi.stubEnv('CODEGRAPH_KERNEL', mode === 'wasm-worker' ? '0' : '1');
vi.stubEnv('CODEGRAPH_NO_STORE_WORKER', mode === 'native-main' ? '1' : '0');
const BuiltCodeGraph: typeof CodeGraph = require(built).default;
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-literal-compiled-'));
fs.writeFileSync(path.join(dir, 'cache.py'), "def persist():\n return 'cache.write'\n");
fs.writeFileSync(path.join(dir, 'siblings.ts'), "/* café 😀 */ export function writer(){return 'sibling.write';} export function reader(){return 'sibling.read';}\n");
cg = await BuiltCodeGraph.init(dir, { silent: true });
const names = (key: string) => cg!.findLiteralSeedIds(key).map(id => cg!.getNode(id)?.name);
const assertOwners = () => {
expect(names('cache.write')).toEqual(['persist']);
expect(names('sibling.write')).toEqual(['writer']);
expect(names('sibling.read')).toEqual(['reader']);
};
await cg.indexAll();
assertOwners();
await cg.indexAll();
assertOwners();

// A schema-only migration has no literal data; unchanged source still needs backfill.
const queries = (cg as unknown as { queries: QueryBuilder }).queries;
queries.replaceLiteralsForFile('cache.py', []);
queries.replaceLiteralsForFile('siblings.ts', []);
queries.setMetadata('indexed_with_extraction_version', '26');
expect(cg.isIndexStale()).toBe(true);
await cg.indexAll();
assertOwners();
expect(cg.isIndexStale()).toBe(false);
fs.rmSync(path.join(dir, 'cache.py'));
await cg.indexAll();
expect(names('cache.write')).toEqual([]);
expect(names('sibling.write')).toEqual(['writer']);
});
}
});
Loading