`storeExtractionResult` deletes a re-indexed file's nodes via `deleteFile`, which cascades through `edges.FK ... ON DELETE CASCADE` to delete every edge whose source OR target is one of those nodes. Edges whose source is in the re-indexed file are re-emitted by the extractor, but edges whose source is in a *different* (unchanged) file are not — they are silently dropped. This is issue #899: re-indexing a callee file severs `calls`/`references` edges from callers that import it via module-attribute access (`pkg.mod.fn(...)`), so `codegraph callers fn` reports 0 callers for functions that have real call sites. A docstring-only edit on the callee is sufficient to trigger it. The bug affects every incremental path that routes through `sync()` / `indexFile()`: `codegraph sync`, the file-watcher auto-sync (which calls `sync()`), and the git sync hooks. `codegraph index` was already fixed by #894 (it now clears-then-rebuilds, so it's a full re-extraction, not incremental). `sync` remains the fast incremental path and still has the bug. Fix: before the delete, snapshot incoming cross-file edges paired with the target node's (name, kind). After re-inserting the file's nodes + same-file edges, re-insert the snapshot — re-resolving each edge's target to the re-indexed node's NEW id by (filePath, kind, name). Node ids are `sha256(filePath:kind:name:line)`, so any line shift in the callee file (e.g. a docstring-only edit above the symbol) changes every target id and a naive re-insert by old id would drop them all. Matching by (kind, name) is stable across line shifts; if the symbol was renamed/removed, no match is found and the edge stays dropped (correct). `insertEdges` still filters to endpoints that exist, so edges whose caller (source) was deleted are also dropped. Regression tests in `__tests__/sync.test.ts` model the RAGFlow production case: a `pkg/mod.py` with two callees, both called from `test/test_callers.py` via `mod.<fn>(...)`. The first test confirms a docstring-only edit that shifts the second callee's line preserves both incoming edges. The second test confirms renaming a callee correctly drops its old incoming edge (no phantom preservation against a non-existent symbol).
This commit is contained in:
@@ -1419,6 +1419,32 @@ export class QueryBuilder {
|
||||
return rows.map((r) => r.fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
getCrossFileIncomingEdgesWithTarget(filePath: string): Array<Edge & { targetName: string; targetKind: NodeKind }> {
|
||||
const sql = `SELECT e.*, tgt.name AS target_name, tgt.kind AS target_kind
|
||||
FROM edges e
|
||||
JOIN nodes tgt ON tgt.id = e.target
|
||||
JOIN nodes src ON src.id = e.source
|
||||
WHERE tgt.file_path = ?
|
||||
AND e.kind != 'contains'
|
||||
AND src.file_path != ?`;
|
||||
const rows = this.db.prepare(sql).all(filePath, filePath) as Array<EdgeRow & { target_name: string; target_kind: NodeKind }>;
|
||||
return rows.map(row => ({
|
||||
...rowToEdge(row),
|
||||
targetName: row.target_name,
|
||||
targetKind: row.target_kind,
|
||||
}));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// File Operations
|
||||
// ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user