mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 06:46:49 +08:00
* fix(sync): same-named symbols keep their own cross-file callers across a re-index (#2276) Re-storing a file re-attaches the edges other files had into it by moving each edge to the re-indexed node that replaces its target. That pairing keyed new nodes by (kind, name) with the last one winning, so in a file defining a name twice — the same method on two classes, a method's overloads — any edit, even a body-only one, moved every incoming caller of those symbols onto one of them. A body-only edit leaves the sync's definition delta empty, so nothing re-resolved them afterwards. The re-store now reads the file's old node identities alongside the edge snapshot and pairs old nodes with new ones by (kind, name), then qualified name, then signature, then position within a same-sized group of otherwise identical nodes. A target no tier can pair one-to-one is resurrected as its original unresolved reference, so resolution rebinds it instead of the store guessing. The common in-memory re-store path also now deletes the old rows, stores the new ones and re-attaches incoming edges in one transaction. Committed separately, a failure between the steps left the file looking new or already current, and other files' edges into it were lost until a full re-index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): same-named symbols keep their callers across a sync (#2276) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
455175da67
commit
e28ac71048
+52
-7
@@ -90,6 +90,15 @@ const IS_INTERFACE_MEMBER = (alias: string): string => `EXISTS (
|
||||
*/
|
||||
export const DEPRIORITIZED_NAME_BONUS_SCALE = 0.75;
|
||||
|
||||
/**
|
||||
* The fields that tell one node of a file from another across a re-index of
|
||||
* that file, whose node ids change with every line shift (#2276).
|
||||
*/
|
||||
export type NodeIdentity = Pick<
|
||||
Node,
|
||||
'id' | 'kind' | 'name' | 'qualifiedName' | 'signature' | 'startLine' | 'startColumn'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Database row types (snake_case from SQLite)
|
||||
*/
|
||||
@@ -290,6 +299,7 @@ export class QueryBuilder {
|
||||
existingNodeIdsFull?: SqliteStatement;
|
||||
getExportedNodesByFile?: SqliteStatement;
|
||||
getNodesByFileAndName?: SqliteStatement;
|
||||
getNodeIdentitiesByFile?: SqliteStatement;
|
||||
getFileNodesByNamePrefix?: SqliteStatement;
|
||||
getNodesByQualifiedNameExact?: SqliteStatement;
|
||||
getNodesByLowerName?: SqliteStatement;
|
||||
@@ -599,6 +609,15 @@ export class QueryBuilder {
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `fn` as one transaction. The write helpers called inside join it
|
||||
* rather than committing on their own, so a crash part-way leaves none of
|
||||
* `fn`'s writes behind.
|
||||
*/
|
||||
runInTransaction<T>(fn: () => T): T {
|
||||
return this.db.transaction(fn)();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store one file's whole extraction bundle — nodes, edges, unresolved refs,
|
||||
* and the file record — in a SINGLE transaction. The bulk-index path calls
|
||||
@@ -1033,6 +1052,31 @@ export class QueryBuilder {
|
||||
return rows.map(rowToNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link NodeIdentity} of every node in a file, without decoding whole
|
||||
* nodes. Read before a re-index deletes the file, so each incoming
|
||||
* cross-file edge can follow its target to the node that replaces it (#2276).
|
||||
*/
|
||||
getNodeIdentitiesByFile(filePath: string): NodeIdentity[] {
|
||||
if (!this.stmts.getNodeIdentitiesByFile) {
|
||||
this.stmts.getNodeIdentitiesByFile = this.db.prepare(
|
||||
'SELECT id, kind, name, qualified_name, signature, start_line, start_column FROM nodes WHERE file_path = ?'
|
||||
);
|
||||
}
|
||||
const rows = this.stmts.getNodeIdentitiesByFile.all(filePath) as Array<
|
||||
Pick<NodeRow, 'id' | 'kind' | 'name' | 'qualified_name' | 'signature' | 'start_line' | 'start_column'>
|
||||
>;
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
kind: row.kind as NodeKind,
|
||||
name: row.name,
|
||||
qualifiedName: row.qualified_name,
|
||||
signature: row.signature ?? undefined,
|
||||
startLine: row.start_line,
|
||||
startColumn: row.start_column,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nodes in several files at once — one chunked `IN` query rather than
|
||||
* one {@link getNodesByFile} per file (#1975).
|
||||
@@ -2920,13 +2964,14 @@ export class QueryBuilder {
|
||||
|
||||
/**
|
||||
* Cross-file edges whose TARGET is a node in `filePath` and whose SOURCE is a
|
||||
* node in a *different* file, paired with the target node's (name, kind) so a
|
||||
* caller can re-resolve the edge to the re-indexed target's new ID (node IDs
|
||||
* are `sha256(filePath:kind:name:line)`, so any line shift in the callee file
|
||||
* changes target IDs and a naive re-insert by old ID silently drops them).
|
||||
* Used by `storeExtractionResult` to preserve incoming edges across a file
|
||||
* re-index (issue #899). Same edge-kind rules as
|
||||
* {@link getDependentFilePaths}: all kinds except `contains`.
|
||||
* node in a *different* file, paired with the target node's (name, kind).
|
||||
* Node IDs are `sha256(filePath:kind:name:line)`, so any line shift in the
|
||||
* callee file changes target IDs and a naive re-insert by old ID silently
|
||||
* drops them; `storeExtractionResult` instead follows each old target to the
|
||||
* re-indexed node that replaces it (see {@link getNodeIdentitiesByFile}) to
|
||||
* preserve incoming edges across a file re-index (issue #899). Same
|
||||
* edge-kind rules as {@link getDependentFilePaths}: all kinds except
|
||||
* `contains`.
|
||||
*/
|
||||
getCrossFileIncomingEdgesWithTarget(
|
||||
filePath: string
|
||||
|
||||
Reference in New Issue
Block a user