diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c8dd8f2a4..c87d0225b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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("/me")` or `await api.get(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()` or `render()`, 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) diff --git a/__tests__/segment-vocab.test.ts b/__tests__/segment-vocab.test.ts index 50d688b53f..fcb2d33733 100644 --- a/__tests__/segment-vocab.test.ts +++ b/__tests__/segment-vocab.test.ts @@ -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'); diff --git a/src/index.ts b/src/index.ts index 73b30f2a19..8d7814e275 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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. @@ -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).