fix(resolution): clean up processed refs by row id so batch boundaries can't drop sibling call sites (#1269) (#1270)
Post-batch cleanup deleted resolved refs (and parked failed ones) by (from_node_id, reference_name, reference_kind) — no line/col. When one caller had several call sites to the same callee and a batch boundary split them, the first batch's cleanup removed every row with that key, including later-batch siblings that were never attempted — their edges were silently never created. On nlohmann/json this ate 422 real call edges (write_cbor's 38 to_char_type calls indexed as 11). Refs loaded from unresolved_refs now carry their row id through resolution, and all three persist paths (sync resolveAndPersist, the yielding retry pass, the batched drain loop) delete / mark-failed by exactly that id. The key-tuple methods remain only as the fallback for hand-built refs from the public API. Failed-parking gains the same precision: outcome can differ per call site (receiver inference reads the ref's line), so a sibling must not inherit another row's failure. Also untracks the zz-scratch local test files that slipped into #1268 and gitignores the pattern. Validation: red-green regression test (5 sites, batch size 2 — old code kept 2 edges, fix keeps 5); nlohmann/json re-index is a strict superset of the previous edge set (0 lost, 422 recovered, spot-checked against source). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6103f5e228
commit
e871c49a31
@@ -1827,6 +1827,7 @@ export class QueryBuilder {
|
||||
candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined,
|
||||
filePath: row.file_path,
|
||||
language: row.language as Language,
|
||||
rowId: row.id,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1844,6 +1845,7 @@ export class QueryBuilder {
|
||||
candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined,
|
||||
filePath: row.file_path,
|
||||
language: row.language as Language,
|
||||
rowId: row.id,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1886,6 +1888,7 @@ export class QueryBuilder {
|
||||
candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined,
|
||||
filePath: row.file_path,
|
||||
language: row.language as Language,
|
||||
rowId: row.id,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1954,6 +1957,7 @@ export class QueryBuilder {
|
||||
candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined,
|
||||
filePath: row.file_path,
|
||||
language: row.language as Language,
|
||||
rowId: row.id,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1998,6 +2002,23 @@ export class QueryBuilder {
|
||||
deleteMany(refs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete unresolved-ref rows by row id — the precise cleanup for refs a
|
||||
* resolution pass actually processed. The key-tuple variant above also
|
||||
* deletes SIBLING rows (same caller calling the same callee at other lines)
|
||||
* that a later batch hasn't attempted yet, so when a batch boundary split a
|
||||
* caller's same-named call sites, the later sites' edges were silently never
|
||||
* created (#1269).
|
||||
*/
|
||||
deleteReferencesByRowIds(rowIds: number[]): void {
|
||||
if (rowIds.length === 0) return;
|
||||
for (let i = 0; i < rowIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
||||
const chunk = rowIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
||||
const placeholders = chunk.map(() => '?').join(',');
|
||||
this.db.prepare(`DELETE FROM unresolved_refs WHERE id IN (${placeholders})`).run(...chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark refs a completed resolution pass could not resolve as status='failed'
|
||||
* instead of deleting them (#1240). Failed rows are invisible to the pending
|
||||
@@ -2020,6 +2041,27 @@ export class QueryBuilder {
|
||||
markMany(refs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park refs as status='failed' by row id — the precise counterpart of
|
||||
* markReferencesFailed, for the same reason as deleteReferencesByRowIds:
|
||||
* the key-tuple variant also flips same-key sibling rows in later batches
|
||||
* to 'failed' before they were ever attempted (#1269). Resolution outcome
|
||||
* can differ per call site (receiver-type inference reads the ref's line),
|
||||
* so a sibling must not inherit this row's failure.
|
||||
*/
|
||||
markReferencesFailedByRowIds(refs: Array<{ rowId: number; referenceName: string }>): void {
|
||||
if (refs.length === 0) return;
|
||||
const stmt = this.db.prepare(
|
||||
"UPDATE unresolved_refs SET status = 'failed', name_tail = ? WHERE id = ?"
|
||||
);
|
||||
const markMany = this.db.transaction((items: typeof refs) => {
|
||||
for (const ref of items) {
|
||||
stmt.run(referenceNameTail(ref.referenceName), ref.rowId);
|
||||
}
|
||||
});
|
||||
markMany(refs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed refs whose name tail matches one of the given symbol names — the
|
||||
* candidates a sync should retry after files carrying those names changed
|
||||
@@ -2068,6 +2110,7 @@ export class QueryBuilder {
|
||||
candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined,
|
||||
filePath: row.file_path,
|
||||
language: row.language as Language,
|
||||
rowId: row.id,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user