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 @@ -14,6 +14,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- Repeating a full index through the library keeps unchanged symbols discoverable by the words in their names, and cancelling a re-index preserves those matches.

- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
- In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600)
Expand Down
37 changes: 37 additions & 0 deletions __tests__/segment-vocab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,43 @@ export function writeConfig(): void {}
expect(hit.kind).not.toBe('file');
});

it('keeps unchanged symbol words through repeated full indexing', async () => {
const before = cg.getSegmentMatches(['state', 'machine']);
expect(before.map((m) => m.name)).toContain('OrderStateMachine');

for (let run = 0; run < 2; run++) {
expect((await cg.indexAll()).success).toBe(true);
expect(cg.getNodesByName('OrderStateMachine')).toHaveLength(1);
expect(cg.getSegmentMatches(['state', 'machine'])).toEqual(before);
expect(cg.getSegmentMatches(['checkout', 'service']).map((m) => m.name)).toContain('CheckoutService');
}
});

it('rebuilds words for both changed and unchanged files after a full index', async () => {
const touched = path.join(dir, 'src', 'state-machine.ts');
fs.writeFileSync(touched, fs.readFileSync(touched, 'utf8').replace('OrderStateMachine', 'RenamedWorkflowEngine'));

expect((await cg.indexAll()).success).toBe(true);
expect(cg.getSegmentMatches(['renamed', 'workflow']).map((m) => m.name)).toContain('RenamedWorkflowEngine');
expect(cg.getSegmentMatches(['checkout', 'service']).map((m) => m.name)).toContain('CheckoutService');
expect(cg.getSegmentMatches(['state', 'machine'])).toEqual([]);

await cg.sync();
expect(cg.getSegmentMatches(['checkout', 'service']).map((m) => m.name)).toContain('CheckoutService');
});

it('preserves existing symbol words when full indexing is cancelled', async () => {
const before = cg.getSegmentMatches(['state', 'machine']);
const controller = new AbortController();
controller.abort();

const result = await cg.indexAll({ signal: controller.signal });
expect(result.success).toBe(false);
expect(result.errors.some((error) => error.message === 'Aborted')).toBe(true);
expect(cg.getNodesByName('OrderStateMachine')).toHaveLength(1);
expect(cg.getSegmentMatches(['state', 'machine'])).toEqual(before);
});

it('single rare word qualifies; ubiquitous and singleton words do not', () => {
// "checkout" clusters (Service + Controller) — a concept this repo is about.
expect(cg.getSegmentMatches(['checkout']).map((m) => m.name)).toContain('CheckoutService');
Expand Down
17 changes: 13 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,10 +575,12 @@ export class CodeGraph {
// marker behind, so `codegraph status` can tell a truncated index
// from a completed one instead of silently serving partial results.
try { this.queries.setMetadata('index_state', 'indexing'); } catch { /* metadata is advisory */ }
// Segment vocabulary starts empty and is repopulated by the node write
// path as every file (re-)indexes below — so a full index is also the
// orphan-cleanup pass for names deleted since the last one.
try { this.queries.clearNameSegmentVocab(); } catch { /* vocab is advisory — never fail an index over it */ }
// Fresh graphs populate the vocabulary through node writes. Existing
// graphs skip unchanged files at the store, so keep their vocabulary
// until this run succeeds and rebuild it from all surviving names below.
if (freshDb) {
try { this.queries.clearNameSegmentVocab(); } catch { /* vocab is advisory — never fail an index over it */ }
}
// Bulk FTS mode for the mass-insert phase: drop the per-row FTS sync
// triggers, rebuild nodes_fts once from the nodes table afterwards.
// Crash inside the window is healed on the next DatabaseConnection.open.
Expand Down Expand Up @@ -785,6 +787,13 @@ export class CodeGraph {
}
} catch { /* metadata is advisory — never fail an index over it */ }

if (result.success && !freshDb) {
try {
this.queries.clearNameSegmentVocab();
await this.rebuildNameSegmentVocab();
} catch { /* vocab is advisory — never fail an index over it */ }
}

// The time covers the whole run, like the totals above. The
// orchestrator's covers extraction alone, and the summary printed it
// as the run's, leaving out resolution and linking (#2334).
Expand Down