mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
fix(impact): order-independent depth>1 bridge evidence
A symbol reachable from multiple parents in the inter-procedural BFS got its proven/unproven bridge label from whichever parent the DB returned first (first-writer-wins), so a diamond-reachable depth≥2 symbol could flip label run-to-run. Compute evidence for every edge, keep the strongest across all parents (callgraph-bridge wins, via betterBridgeEvidence), and stamp the finalized label onto the impacted items after the depth loop. The label is now deterministic; reach is unaffected. Addresses PR #2227 tri-review finding (P3, correctness + adversarial). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ed88b330f
commit
3618ee62cf
2 changed files with 64 additions and 16 deletions
|
|
@ -309,6 +309,24 @@ function pdgBridgeEvidenceForImpact(input: {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the stronger of two bridge-evidence verdicts for the same reached symbol.
|
||||
* `callgraph-bridge` (proven) beats `unproven-bridge`, so a node reachable from
|
||||
* multiple parents is proven if ANY parent proves it. This makes the
|
||||
* proven/unproven label order-independent of DB row iteration — a diamond-reached
|
||||
* symbol gets the same label regardless of which parent the BFS visits first
|
||||
* (PR #2227 tri-review, P3).
|
||||
*/
|
||||
export function betterBridgeEvidence(
|
||||
existing: PdgBridgeEvidenceInfo | undefined,
|
||||
candidate: PdgBridgeEvidenceInfo,
|
||||
): PdgBridgeEvidenceInfo {
|
||||
if (!existing) return candidate;
|
||||
if (existing.evidence === 'callgraph-bridge') return existing;
|
||||
if (candidate.evidence === 'callgraph-bridge') return candidate;
|
||||
return existing;
|
||||
}
|
||||
|
||||
function normalizePdgBridgeByDepth(byDepth: Record<number, unknown[]>): Record<number, unknown[]> {
|
||||
const normalized: Record<number, unknown[]> = {};
|
||||
for (const [depthKey, items] of Object.entries(byDepth ?? {})) {
|
||||
|
|
@ -5340,27 +5358,37 @@ export class LocalBackend {
|
|||
|
||||
if (!includeTests && isTestFilePath(filePath)) continue;
|
||||
|
||||
// Bridge evidence is computed for EVERY edge (not just the first to
|
||||
// reach a node) and the strongest verdict across all parents is kept
|
||||
// (`callgraph-bridge` wins). This makes a diamond-reachable node's
|
||||
// proven/unproven label order-independent of DB row iteration; the
|
||||
// final label is stamped onto the impacted items after the depth loop.
|
||||
if (opts.pdgBridge) {
|
||||
const ev = pdgBridgeEvidenceForImpact({
|
||||
bridge: opts.pdgBridge,
|
||||
depth,
|
||||
calleeName: rel.name || rel[2],
|
||||
inherited: pdgBridgeEvidenceById.get(sourceId),
|
||||
});
|
||||
pdgBridgeEvidenceById.set(
|
||||
String(relId),
|
||||
betterBridgeEvidence(pdgBridgeEvidenceById.get(String(relId)), ev),
|
||||
);
|
||||
}
|
||||
|
||||
if (!visited.has(relId)) {
|
||||
visited.add(relId);
|
||||
nextFrontier.push(relId);
|
||||
const storedConfidence = rel.confidence ?? rel[6];
|
||||
const relationType = rel.relType || rel[5];
|
||||
const calleeName = rel.name || rel[2];
|
||||
const bridgeEvidence = opts.pdgBridge
|
||||
? pdgBridgeEvidenceForImpact({
|
||||
bridge: opts.pdgBridge,
|
||||
depth,
|
||||
calleeName,
|
||||
inherited: pdgBridgeEvidenceById.get(sourceId),
|
||||
})
|
||||
: undefined;
|
||||
if (bridgeEvidence) pdgBridgeEvidenceById.set(String(relId), bridgeEvidence);
|
||||
// Prefer the stored confidence from the graph (set at analysis time);
|
||||
// fall back to the per-type floor for edges without a stored value.
|
||||
const effectiveConfidence =
|
||||
typeof storedConfidence === 'number' && storedConfidence > 0
|
||||
? storedConfidence
|
||||
: confidenceForRelType(relationType);
|
||||
// pdgEvidence is stamped after the depth loop from the finalized,
|
||||
// order-independent pdgBridgeEvidenceById map.
|
||||
impacted.push({
|
||||
depth,
|
||||
id: relId,
|
||||
|
|
@ -5369,12 +5397,6 @@ export class LocalBackend {
|
|||
filePath,
|
||||
relationType,
|
||||
confidence: effectiveConfidence,
|
||||
...(bridgeEvidence
|
||||
? {
|
||||
pdgEvidence: bridgeEvidence.evidence,
|
||||
pdgBridgeBasis: bridgeEvidence.basis,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -5389,6 +5411,19 @@ export class LocalBackend {
|
|||
frontier = nextFrontier;
|
||||
}
|
||||
|
||||
// Stamp the finalized, order-independent bridge evidence (strongest across
|
||||
// all parents) onto each impacted item. Deferred from the BFS loop so a
|
||||
// diamond-reachable node reflects a proven parent regardless of visit order.
|
||||
if (opts.pdgBridge) {
|
||||
for (const item of impacted as Array<Record<string, unknown>>) {
|
||||
const ev = pdgBridgeEvidenceById.get(String(item.id));
|
||||
if (ev) {
|
||||
item.pdgEvidence = ev.evidence;
|
||||
item.pdgBridgeBasis = ev.basis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const grouped: Record<number, any[]> = {};
|
||||
for (const item of impacted) {
|
||||
if (!grouped[item.depth]) grouped[item.depth] = [];
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ import {
|
|||
LocalBackend,
|
||||
REPO_ID_HASH_LENGTH,
|
||||
parseListReposPagination,
|
||||
betterBridgeEvidence,
|
||||
} from '../../src/mcp/local/local-backend.js';
|
||||
import {
|
||||
listRegisteredRepos,
|
||||
|
|
@ -1705,6 +1706,18 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
expect([...bridge.sliceCalleeNames]).toContain('seedCallee');
|
||||
});
|
||||
|
||||
it('betterBridgeEvidence keeps callgraph-bridge regardless of parent order (U3 order-independence)', () => {
|
||||
const proven = { evidence: 'callgraph-bridge' as const, basis: 'in slice' };
|
||||
const unproven = { evidence: 'unproven-bridge' as const, basis: 'not in slice' };
|
||||
// A node reached from a proven and an unproven parent is proven either way —
|
||||
// the diamond label does not depend on which edge the BFS visits first.
|
||||
expect(betterBridgeEvidence(unproven, proven).evidence).toBe('callgraph-bridge');
|
||||
expect(betterBridgeEvidence(proven, unproven).evidence).toBe('callgraph-bridge');
|
||||
// First verdict wins when neither is stronger; undefined existing takes the candidate.
|
||||
expect(betterBridgeEvidence(undefined, unproven).evidence).toBe('unproven-bridge');
|
||||
expect(betterBridgeEvidence(unproven, unproven).evidence).toBe('unproven-bridge');
|
||||
});
|
||||
|
||||
it("mode:'pdg' + crossDepth → hard {error} (single-repo PDG impact)", async () => {
|
||||
resolveSingleTarget();
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue