mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(impact): label pdg bridge evidence
This commit is contained in:
parent
a9fd55a91b
commit
d9d702dc4d
5 changed files with 313 additions and 7 deletions
|
|
@ -75,6 +75,7 @@ import {
|
|||
runImpactPDG,
|
||||
validateImpactMode,
|
||||
type ImpactMode,
|
||||
type PdgImpactEvidence,
|
||||
type PdgImpactResult,
|
||||
} from './pdg-impact.js';
|
||||
|
||||
|
|
@ -250,6 +251,141 @@ export const IMPACT_RELATION_CONFIDENCE: Readonly<Record<string, number>> = {
|
|||
const confidenceForRelType = (relType: string | undefined): number =>
|
||||
IMPACT_RELATION_CONFIDENCE[relType ?? ''] ?? 0.5;
|
||||
|
||||
type PdgBridgeEvidence = Extract<PdgImpactEvidence, 'callgraph-bridge' | 'unproven-bridge'>;
|
||||
|
||||
interface PdgBridgeEvidenceInfo {
|
||||
evidence: PdgBridgeEvidence;
|
||||
basis: string;
|
||||
site?: { filePath: string; line: number };
|
||||
}
|
||||
|
||||
interface PdgBridgeOptions {
|
||||
/** Line keys (`<filePath>:<line>`) proven reachable by the local PDG slice. */
|
||||
firstHopLineKeys?: ReadonlySet<string>;
|
||||
/** File containing the first-hop call sites for a line-seeded downstream slice. */
|
||||
targetFilePath?: string;
|
||||
}
|
||||
|
||||
function parseRelationSiteLine(relationId: unknown): number | undefined {
|
||||
const id = String(relationId ?? '');
|
||||
if (!id) return undefined;
|
||||
const parts = id.split(':');
|
||||
const maybeLine = Number(parts[parts.length - 2]);
|
||||
const maybeCol = Number(parts[parts.length - 1]);
|
||||
if (Number.isInteger(maybeLine) && maybeLine >= 1 && Number.isInteger(maybeCol)) {
|
||||
return maybeLine;
|
||||
}
|
||||
const cobolLine = id.match(/:L(\d+)(?::|$)/i);
|
||||
if (cobolLine) {
|
||||
const line = Number(cobolLine[1]);
|
||||
if (Number.isInteger(line) && line >= 1) return line;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function lineKey(filePath: string | undefined, line: number | undefined): string | undefined {
|
||||
if (!filePath || !Number.isInteger(line) || (line as number) < 1) return undefined;
|
||||
return `${filePath}:${line}`;
|
||||
}
|
||||
|
||||
function pdgBridgeEvidenceForImpact(input: {
|
||||
bridge: PdgBridgeOptions;
|
||||
depth: number;
|
||||
relationId: unknown;
|
||||
inherited?: PdgBridgeEvidenceInfo;
|
||||
}): PdgBridgeEvidenceInfo {
|
||||
const { bridge, depth, relationId, inherited } = input;
|
||||
if (depth > 1) {
|
||||
return (
|
||||
inherited ?? {
|
||||
evidence: 'unproven-bridge',
|
||||
basis: 'first-hop evidence unavailable for inherited symbol-graph reach',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const firstHopLineKeys = bridge.firstHopLineKeys;
|
||||
if (!firstHopLineKeys || firstHopLineKeys.size === 0) {
|
||||
return {
|
||||
evidence: 'callgraph-bridge',
|
||||
basis: 'whole-symbol PDG result uses symbol graph as compatibility bridge',
|
||||
};
|
||||
}
|
||||
|
||||
const siteLine = parseRelationSiteLine(relationId);
|
||||
const siteKey = lineKey(bridge.targetFilePath, siteLine);
|
||||
if (siteKey && firstHopLineKeys.has(siteKey)) {
|
||||
return {
|
||||
evidence: 'callgraph-bridge',
|
||||
basis: 'first-hop call site is in the local PDG statement slice',
|
||||
site: { filePath: bridge.targetFilePath!, line: siteLine! },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evidence: 'unproven-bridge',
|
||||
basis: siteLine
|
||||
? 'first-hop call site is not in the local PDG statement slice'
|
||||
: 'first-hop call-site line is unavailable on the existing graph edge',
|
||||
...(siteLine && bridge.targetFilePath
|
||||
? { site: { filePath: bridge.targetFilePath, line: siteLine } }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePdgBridgeByDepth(byDepth: Record<number, unknown[]>): Record<number, unknown[]> {
|
||||
const normalized: Record<number, unknown[]> = {};
|
||||
for (const [depthKey, items] of Object.entries(byDepth ?? {})) {
|
||||
const depth = Number(depthKey);
|
||||
if (!Number.isFinite(depth) || !Array.isArray(items)) continue;
|
||||
normalized[depth] = items.map((item) => {
|
||||
if (!item || typeof item !== 'object') return item;
|
||||
const record = item as Record<string, unknown>;
|
||||
const evidence =
|
||||
typeof record.pdgEvidence === 'string'
|
||||
? (record.pdgEvidence as PdgImpactEvidence)
|
||||
: 'callgraph-bridge';
|
||||
return {
|
||||
...record,
|
||||
pdgEvidence: evidence,
|
||||
...(record.pdgEvidenceReason
|
||||
? {}
|
||||
: {
|
||||
pdgEvidenceReason:
|
||||
evidence === 'unproven-bridge'
|
||||
? 'symbol reached through the resolved symbol graph, but the existing graph did not prove the first-hop call site is in the local PDG slice'
|
||||
: 'symbol reached through the resolved symbol graph compatibility bridge',
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function countPdgEvidence(
|
||||
byDepth: Record<number, unknown[]>,
|
||||
): Partial<Record<PdgImpactEvidence, number>> {
|
||||
const counts: Partial<Record<PdgImpactEvidence, number>> = {};
|
||||
for (const items of Object.values(byDepth ?? {})) {
|
||||
if (!Array.isArray(items)) continue;
|
||||
for (const item of items) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const evidence = (item as { pdgEvidence?: unknown }).pdgEvidence;
|
||||
if (typeof evidence !== 'string') continue;
|
||||
counts[evidence as PdgImpactEvidence] = (counts[evidence as PdgImpactEvidence] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function dominantInterproceduralEvidence(
|
||||
counts: Partial<Record<PdgImpactEvidence, number>>,
|
||||
): PdgBridgeEvidence | undefined {
|
||||
if ((counts['unproven-bridge'] ?? 0) > 0) return 'unproven-bridge';
|
||||
if ((counts['callgraph-bridge'] ?? 0) > 0) return 'callgraph-bridge';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Structured error logging for query failures — replaces empty catch blocks */
|
||||
function logQueryError(context: string, err: unknown): void {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
|
|
@ -4628,6 +4764,22 @@ export class LocalBackend {
|
|||
executeParameterized,
|
||||
});
|
||||
|
||||
const firstHopLineKeys = new Set<string>();
|
||||
if (typeof (pdgResult as any).criterionLine === 'number') {
|
||||
firstHopLineKeys.add(`${sym.filePath}:${(pdgResult as any).criterionLine}`);
|
||||
}
|
||||
for (const stmt of ((pdgResult as any).affectedStatements ?? []) as Array<{
|
||||
filePath?: string;
|
||||
line?: number;
|
||||
}>) {
|
||||
const key = lineKey(stmt.filePath, stmt.line);
|
||||
if (key) firstHopLineKeys.add(key);
|
||||
}
|
||||
const pdgBridge =
|
||||
direction === 'downstream' && firstHopLineKeys.size > 0
|
||||
? { firstHopLineKeys, targetFilePath: sym.filePath }
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const interproceduralResult = await this._runImpactBFS(repo, sym, symType, direction, {
|
||||
maxDepth,
|
||||
|
|
@ -4636,6 +4788,7 @@ export class LocalBackend {
|
|||
minConfidence,
|
||||
limit: Number.isFinite(params.limit) ? params.limit : 100,
|
||||
offset: Number.isFinite(params.offset) ? params.offset : 0,
|
||||
pdgBridge,
|
||||
});
|
||||
return this.composeUnifiedPdgImpactResult(pdgResult, interproceduralResult);
|
||||
} catch (e) {
|
||||
|
|
@ -4698,8 +4851,10 @@ export class LocalBackend {
|
|||
|
||||
const localByDepth = pdgResult.byDepth ?? {};
|
||||
const localByDepthCounts = pdgResult.byDepthCounts ?? {};
|
||||
const interproceduralByDepth = interproceduralResult?.byDepth ?? {};
|
||||
const interproceduralByDepth = normalizePdgBridgeByDepth(interproceduralResult?.byDepth ?? {});
|
||||
const interproceduralByDepthCounts = interproceduralResult?.byDepthCounts ?? {};
|
||||
const interproceduralEvidenceCounts = countPdgEvidence(interproceduralByDepth);
|
||||
const interproceduralEvidence = dominantInterproceduralEvidence(interproceduralEvidenceCounts);
|
||||
const byDepth: Record<number, unknown[]> = {};
|
||||
const byDepthCounts: Record<number, number> = {};
|
||||
const depthKeys = Array.from(
|
||||
|
|
@ -4769,7 +4924,8 @@ export class LocalBackend {
|
|||
const noteParts = [
|
||||
pdgResult.note,
|
||||
`Inter-procedural symbol reach is included using the resolved symbol graph; ` +
|
||||
`statement-level PDG reach remains in affectedStatements.`,
|
||||
`statement-level PDG reach remains in affectedStatements. The symbol reach is ` +
|
||||
`labeled as a PDG evidence bridge, not as pure statement-level dependence.`,
|
||||
];
|
||||
if (errorMessage) {
|
||||
noteParts.push(
|
||||
|
|
@ -4780,6 +4936,13 @@ export class LocalBackend {
|
|||
`The inter-procedural symbol reach is a lower bound because unresolved indirection was detected.`,
|
||||
);
|
||||
}
|
||||
if ((interproceduralEvidenceCounts['unproven-bridge'] ?? 0) > 0) {
|
||||
noteParts.push(
|
||||
`${interproceduralEvidenceCounts['unproven-bridge']} inter-procedural ` +
|
||||
`symbol(s) are labeled unproven-bridge: the resolved symbol graph reaches them, ` +
|
||||
`but the current graph did not prove their first-hop call site is in the local PDG slice.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...pdgResult,
|
||||
|
|
@ -4800,11 +4963,18 @@ export class LocalBackend {
|
|||
? { interproceduralBoundaries: interproceduralResult.boundaries }
|
||||
: {}),
|
||||
...(errorMessage ? { interproceduralError: errorMessage } : {}),
|
||||
pdgEvidence: {
|
||||
...((pdgResult as any).pdgEvidence ?? {}),
|
||||
...(interproceduralEvidence ? { interprocedural: interproceduralEvidence } : {}),
|
||||
interproceduralEvidenceCounts,
|
||||
},
|
||||
pdgInterprocedural: {
|
||||
engine: 'symbol-graph',
|
||||
evidence: interproceduralEvidence ?? 'callgraph-bridge',
|
||||
impactedCount: interproceduralImpactedCount,
|
||||
byDepthCounts: interproceduralByDepthCounts,
|
||||
byDepth: interproceduralByDepth,
|
||||
evidenceCounts: interproceduralEvidenceCounts,
|
||||
partial,
|
||||
},
|
||||
};
|
||||
|
|
@ -4959,6 +5129,7 @@ export class LocalBackend {
|
|||
skipPerSymbolEnrichment?: boolean;
|
||||
skipEpistemic?: boolean;
|
||||
skipEnrichment?: boolean;
|
||||
pdgBridge?: PdgBridgeOptions;
|
||||
},
|
||||
): Promise<any> {
|
||||
const { maxDepth, relationTypes, includeTests, minConfidence } = opts;
|
||||
|
|
@ -5001,6 +5172,7 @@ export class LocalBackend {
|
|||
|
||||
const impacted: any[] = [];
|
||||
const visited = new Set<string>([symId]);
|
||||
const pdgBridgeEvidenceById = new Map<string, PdgBridgeEvidenceInfo>();
|
||||
let frontier = [symId];
|
||||
let traversalComplete = true;
|
||||
|
||||
|
|
@ -5087,8 +5259,8 @@ export class LocalBackend {
|
|||
// ids/types/confidence are bound parameters (see above) — no interpolation.
|
||||
const query =
|
||||
direction === 'upstream'
|
||||
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN $frontierIds AND r.type IN $relTypes${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
|
||||
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN $frontierIds AND r.type IN $relTypes${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
|
||||
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN $frontierIds AND r.type IN $relTypes${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence, r.reason AS relationReason`
|
||||
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN $frontierIds AND r.type IN $relTypes${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence, r.reason AS relationReason`;
|
||||
|
||||
try {
|
||||
const related = await executeParameterized(repo.lbugPath, query, {
|
||||
|
|
@ -5098,6 +5270,7 @@ export class LocalBackend {
|
|||
});
|
||||
|
||||
for (const rel of related) {
|
||||
const sourceId = String(rel.sourceId ?? rel[0] ?? '');
|
||||
const relId = rel.id || rel[1];
|
||||
const filePath = rel.filePath || rel[4] || '';
|
||||
|
||||
|
|
@ -5108,6 +5281,16 @@ export class LocalBackend {
|
|||
nextFrontier.push(relId);
|
||||
const storedConfidence = rel.confidence ?? rel[6];
|
||||
const relationType = rel.relType || rel[5];
|
||||
const relationId = rel.relationId ?? rel[7];
|
||||
const bridgeEvidence = opts.pdgBridge
|
||||
? pdgBridgeEvidenceForImpact({
|
||||
bridge: opts.pdgBridge,
|
||||
depth,
|
||||
relationId,
|
||||
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 =
|
||||
|
|
@ -5122,6 +5305,13 @@ export class LocalBackend {
|
|||
filePath,
|
||||
relationType,
|
||||
confidence: effectiveConfidence,
|
||||
...(bridgeEvidence
|
||||
? {
|
||||
pdgEvidence: bridgeEvidence.evidence,
|
||||
pdgBridgeBasis: bridgeEvidence.basis,
|
||||
...(bridgeEvidence.site ? { pdgBridgeSite: bridgeEvidence.site } : {}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,11 +298,30 @@ export interface PdgImpactParityFields {
|
|||
affected_modules: unknown[];
|
||||
}
|
||||
|
||||
export type PdgImpactEvidence =
|
||||
| 'local-dependence'
|
||||
| 'owner-projection'
|
||||
| 'callgraph-bridge'
|
||||
| 'unproven-bridge'
|
||||
| 'degraded';
|
||||
|
||||
export interface PdgImpactEvidenceSummary {
|
||||
statements?: PdgImpactEvidence;
|
||||
localSymbols?: PdgImpactEvidence;
|
||||
interprocedural?: PdgImpactEvidence;
|
||||
localSymbolCount?: number;
|
||||
unresolvedBlockCount?: number;
|
||||
ambiguousProjectionCount?: number;
|
||||
interproceduralEvidenceCounts?: Partial<Record<PdgImpactEvidence, number>>;
|
||||
}
|
||||
|
||||
export interface PdgInterproceduralImpact {
|
||||
engine: 'symbol-graph';
|
||||
evidence: Extract<PdgImpactEvidence, 'callgraph-bridge' | 'unproven-bridge'>;
|
||||
impactedCount: number;
|
||||
byDepthCounts: Record<number, number>;
|
||||
byDepth: Record<number, unknown[]>;
|
||||
evidenceCounts?: Partial<Record<PdgImpactEvidence, number>>;
|
||||
partial: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -320,6 +339,7 @@ export interface PdgImpactBaseResult extends PdgImpactParityFields {
|
|||
interproceduralBoundaries?: unknown[];
|
||||
interproceduralError?: string;
|
||||
pdgInterprocedural?: PdgInterproceduralImpact;
|
||||
pdgEvidence?: PdgImpactEvidenceSummary;
|
||||
}
|
||||
|
||||
export interface PdgImpactSuccessResult extends PdgImpactBaseResult {
|
||||
|
|
@ -493,6 +513,11 @@ function assemblePdgImpactResult(input: {
|
|||
...(s.startLine !== undefined ? { startLine: s.startLine } : {}),
|
||||
...(s.ambiguous ? { ambiguous: true } : {}),
|
||||
...(s.id === null ? { unresolved: true } : {}),
|
||||
pdgEvidence: (s.id === null ? 'degraded' : 'owner-projection') as PdgImpactEvidence,
|
||||
pdgEvidenceReason:
|
||||
s.id === null
|
||||
? 'reachable BasicBlock has no owning Function/Method/Constructor projection'
|
||||
: 'reachable BasicBlock projected to its owning symbol',
|
||||
processes: [] as unknown[],
|
||||
}));
|
||||
|
||||
|
|
@ -547,6 +572,13 @@ function assemblePdgImpactResult(input: {
|
|||
// PDG-specific epistemic marker — NOT the callgraph 'lower-bound'/DI copy.
|
||||
epistemic: 'pdg-intra-procedural',
|
||||
note: noteParts.join(' '),
|
||||
pdgEvidence: {
|
||||
statements: 'local-dependence',
|
||||
localSymbols: unresolvedCount > 0 ? 'degraded' : 'owner-projection',
|
||||
localSymbolCount: impactedCount,
|
||||
unresolvedBlockCount: unresolvedCount,
|
||||
ambiguousProjectionCount: ambiguousCount,
|
||||
},
|
||||
// Statement-level slice: the dependent source statements (line + text) the
|
||||
// change reaches. This is the primary useful output of statement mode; the
|
||||
// accuracy harness scores against these lines.
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|||
enum: ['callgraph', 'pdg'],
|
||||
default: 'callgraph',
|
||||
description:
|
||||
"Blast-radius engine. 'callgraph' (default) = inter-procedural symbol→symbol traversal (established comparator). 'pdg' = unified PDG-facing impact: statement-level affectedStatements from the persisted control/data dependence layer plus inter-procedural symbols in interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket; requires `gitnexus analyze --pdg`. PDG is incompatible with crossDepth and @group targets; relationTypes/minConfidence filter the inter-symbol reach.",
|
||||
"Blast-radius engine. 'callgraph' (default) = inter-procedural symbol→symbol traversal (established comparator). 'pdg' = unified PDG-facing impact: intra-procedural statement-level affectedStatements from the persisted control/data dependence layer plus inter-procedural symbols in interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket; requires `gitnexus analyze --pdg`. PDG symbol reach is labeled as a PDG evidence bridge, not pure statement-level dependence, and successful PDG results are UNKNOWN-risk. PDG is incompatible with crossDepth and @group targets; relationTypes/minConfidence filter the inter-symbol reach.",
|
||||
},
|
||||
line: {
|
||||
type: 'integer',
|
||||
|
|
|
|||
|
|
@ -1479,6 +1479,84 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("mode:'pdg' labels interprocedural symbols as a callgraph bridge", async () => {
|
||||
resolveSingleTarget();
|
||||
vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({
|
||||
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
|
||||
direction: 'downstream',
|
||||
impactedCount: 1,
|
||||
risk: 'LOW',
|
||||
summary: { direct: 1, processes_affected: 0, modules_affected: 0 },
|
||||
byDepthCounts: { 1: 1 },
|
||||
affected_processes: [],
|
||||
affected_modules: [],
|
||||
byDepth: {
|
||||
1: [
|
||||
{
|
||||
depth: 1,
|
||||
id: 'func:callee',
|
||||
name: 'callee',
|
||||
type: 'Function',
|
||||
filePath: 'src/callee.ts',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'main',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(result.pdgInterprocedural.evidence).toBe('callgraph-bridge');
|
||||
expect(result.pdgInterprocedural.evidenceCounts['callgraph-bridge']).toBe(1);
|
||||
expect(result.pdgEvidence.interprocedural).toBe('callgraph-bridge');
|
||||
expect(result.interproceduralByDepth[1][0].pdgEvidence).toBe('callgraph-bridge');
|
||||
expect(result.note).toContain('labeled as a PDG evidence bridge');
|
||||
});
|
||||
|
||||
it("mode:'pdg' preserves unproven bridge evidence when call-site proof is unavailable", async () => {
|
||||
resolveSingleTarget();
|
||||
vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({
|
||||
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
|
||||
direction: 'downstream',
|
||||
impactedCount: 1,
|
||||
risk: 'LOW',
|
||||
summary: { direct: 1, processes_affected: 0, modules_affected: 0 },
|
||||
byDepthCounts: { 1: 1 },
|
||||
affected_processes: [],
|
||||
affected_modules: [],
|
||||
byDepth: {
|
||||
1: [
|
||||
{
|
||||
depth: 1,
|
||||
id: 'func:callee',
|
||||
name: 'callee',
|
||||
type: 'Function',
|
||||
filePath: 'src/callee.ts',
|
||||
pdgEvidence: 'unproven-bridge',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'main',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(result.pdgInterprocedural.evidence).toBe('unproven-bridge');
|
||||
expect(result.pdgInterprocedural.evidenceCounts['unproven-bridge']).toBe(1);
|
||||
expect(result.pdgEvidence.interprocedural).toBe('unproven-bridge');
|
||||
expect(result.note).toContain('labeled unproven-bridge');
|
||||
});
|
||||
|
||||
it.each([['PDG'], ['pgd'], [''], [0], [null]])(
|
||||
'invalid mode %j → structured {error}, never a callgraph result (KTD5 anti-silent-fallback)',
|
||||
async (bad) => {
|
||||
|
|
@ -1532,13 +1610,13 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("mode:'pdg' + line:8 routes to the PDG traversal and interprocedural reach", async () => {
|
||||
it("mode:'pdg' + downstream line:8 routes to the PDG traversal and seeds bridge evidence", async () => {
|
||||
resolveSingleTarget();
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
const pdgSpy = vi.spyOn(backend as any, '_runImpactPDG');
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'main',
|
||||
direction: 'upstream',
|
||||
direction: 'downstream',
|
||||
mode: 'pdg',
|
||||
line: 8,
|
||||
});
|
||||
|
|
@ -1547,6 +1625,9 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
expect(result.mode).toBe('pdg');
|
||||
expect(pdgSpy).toHaveBeenCalledTimes(1);
|
||||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
const bridge = bfsSpy.mock.calls[0][4].pdgBridge;
|
||||
expect(bridge.targetFilePath).toBe('src/index.ts');
|
||||
expect([...bridge.firstHopLineKeys]).toContain('src/index.ts:8');
|
||||
expect(result.pdgInterprocedural).toBeDefined();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ describe('runImpactPDG', () => {
|
|||
expect((result as any).affectedStatementCount).toBe(2);
|
||||
expect((result as any).affectedStatements.map((s: any) => s.line)).toEqual([2, 2]);
|
||||
expect((result as any).affectedStatements.map((s: any) => s.text)).toEqual(['a();', 'b();']);
|
||||
expect((result as any).pdgEvidence.statements).toBe('local-dependence');
|
||||
expect((result as any).pdgEvidence.localSymbols).toBe('owner-projection');
|
||||
expect((result as any).byDepth[1][0].pdgEvidence).toBe('owner-projection');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue