mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
* docs(plans): add impact file risk plan Capture the evidence, constraints, and verification path for fixing incomparable File and symbol impact risk. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(impact): centralize risk scoring Keep the existing thresholds in one shared scorer and expose a common-axis comparison for targets with unavailable enrichment axes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(impact): expose incomparable file risk scale Mark File impact results when process and module axes are unavailable, and provide a common-axis score for honest cross-kind comparisons. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(impact): explain cross-kind risk comparisons Surface the common-axis score in CLI and agent guidance while reusing the shared threshold ladder in the web impact tool. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(impact): fail closed when enrichment is incomplete Preserve proved HIGH/CRITICAL process counts, treat failed queries as UNKNOWN, and surface riskScale metadata on MCP, group, CLI, and Graph-RAG File walks. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
559 lines
20 KiB
TypeScript
559 lines
20 KiB
TypeScript
/**
|
|
* U5 — CLI / consumer rendering for PDG (`mode:'pdg'`) impact results.
|
|
*
|
|
* Guards the KTD8 presentation contract: PDG results must render HONESTLY —
|
|
* - inter-procedural symbol reach under a neutral heading, NOT callgraph severity labels;
|
|
* - the unified PDG caveat (statement reach in affectedStatements, symbol reach in interproceduralByDepth/byDepth);
|
|
* - degradation → the "run analyze --pdg" remediation, NOT a zero blast radius;
|
|
* - no-body (KTD6) → the "not applicable to this symbol kind" caveat, NOT
|
|
* "isolated / no dependencies";
|
|
* - the callgraph DI/dynamic-dispatch epistemic copy is NEVER printed for PDG.
|
|
*
|
|
* And the standing interchangeability contract (KTD8): `mode:'callgraph'`
|
|
* rendering stays byte-identical (regression guard).
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { formatImpactResult, getNextStepHint } from '../../src/cli/eval-server.js';
|
|
|
|
// A representative PDG findings result, shaped exactly like
|
|
// `assemblePdgImpactResult` (pdg-impact.ts) emits.
|
|
function pdgFindings(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
const items = [
|
|
{
|
|
depth: 1,
|
|
id: 'Function:src/svc.ts:applyDiscount',
|
|
name: 'applyDiscount',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
processes: [],
|
|
},
|
|
{
|
|
depth: 1,
|
|
id: 'Function:src/svc.ts:finalizeTotal',
|
|
name: 'finalizeTotal',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
processes: [],
|
|
},
|
|
];
|
|
return {
|
|
mode: 'pdg',
|
|
pdgResultVersion: 3,
|
|
target: {
|
|
id: 'Function:src/svc.ts:computeTotal',
|
|
name: 'computeTotal',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
},
|
|
direction: 'downstream',
|
|
impactedCount: 2,
|
|
risk: 'UNKNOWN',
|
|
epistemic: 'pdg-intra-procedural',
|
|
note:
|
|
"mode:'pdg' — intra-procedural Program Dependence Graph. 2 owning symbols reached via 4 " +
|
|
'dependence blocks (downstream over CDG + REACHING_DEF). Inter-procedural symbol reach ' +
|
|
'is included using the resolved symbol graph; statement-level PDG reach remains in affectedStatements.',
|
|
reachableBlocks: ['b1', 'b2', 'b3', 'b4'],
|
|
blockCount: 4,
|
|
depthReached: 2,
|
|
unresolvedBlockCount: 0,
|
|
ambiguousProjectionCount: 0,
|
|
summary: { direct: 2, processes_affected: 0, modules_affected: 0 },
|
|
byDepthCounts: { 1: 2 },
|
|
affected_processes: [],
|
|
affected_modules: [],
|
|
byDepth: { 1: items },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|
it('renders a PDG ambiguous target without fabricated zero blast-radius counts', () => {
|
|
const out = formatImpactResult({
|
|
status: 'ambiguous',
|
|
mode: 'pdg',
|
|
message:
|
|
"Found 2 symbols matching 'login'. Disambiguate with target_uid for a single authoritative PDG result.",
|
|
target: { name: 'login' },
|
|
direction: 'upstream',
|
|
totalCandidates: 2,
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
candidates: [
|
|
{
|
|
uid: 'func:login:1',
|
|
name: 'login',
|
|
kind: 'Function',
|
|
filePath: 'src/auth.ts',
|
|
line: 5,
|
|
score: 1,
|
|
},
|
|
{
|
|
uid: 'func:login:2',
|
|
name: 'login',
|
|
kind: 'Function',
|
|
filePath: 'src/admin/login.ts',
|
|
line: 8,
|
|
score: 0.91,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(out).toContain('login: AMBIGUOUS');
|
|
expect(out).toContain('PDG impact was not computed');
|
|
expect(out).toContain('func:login:1');
|
|
expect(out).not.toContain('Max blast radius 0');
|
|
expect(out).not.toContain('[0 upstream');
|
|
});
|
|
|
|
it('renders unified PDG symbol reach without callgraph severity labels', () => {
|
|
const out = formatImpactResult(pdgFindings());
|
|
|
|
// PDG framing — NOT the callgraph "depth N / WILL BREAK (direct)" labels.
|
|
expect(out).toContain('Inter-procedural symbol reach');
|
|
expect(out).toContain('d=1 (2)');
|
|
expect(out).not.toContain('WILL BREAK (direct)');
|
|
expect(out).not.toContain('LIKELY AFFECTED');
|
|
expect(out).not.toContain('MAY NEED TESTING');
|
|
// The callgraph "Blast radius for ... will break if changed" headline must
|
|
// not leak into PDG output.
|
|
expect(out).not.toContain('Blast radius for');
|
|
|
|
// The affected symbols are listed.
|
|
expect(out).toContain('applyDiscount');
|
|
expect(out).toContain('finalizeTotal');
|
|
expect(out).toContain('src/svc.ts');
|
|
|
|
// The unified contract is present.
|
|
expect(out).toContain('statement-level PDG reach remains in affectedStatements');
|
|
|
|
// The callgraph DI / dynamic-dispatch lower-bound copy must NEVER appear.
|
|
expect(out).not.toContain('dynamic dispatch');
|
|
expect(out).not.toContain('binding via DI');
|
|
});
|
|
|
|
it('carries the stable pdgResultVersion discriminator on the findings result', () => {
|
|
// The PDG result family advertises a contract version (FIX #2) so external
|
|
// MCP/agent consumers can version against future shape evolution. It is a
|
|
// mode:'pdg'-only field — never on the default callgraph result.
|
|
expect(pdgFindings()).toMatchObject({ mode: 'pdg', pdgResultVersion: 3 });
|
|
});
|
|
|
|
it('surfaces ambiguous-projection and unresolved block counts honestly', () => {
|
|
const out = formatImpactResult(
|
|
pdgFindings({
|
|
ambiguousProjectionCount: 2,
|
|
unresolvedBlockCount: 1,
|
|
byDepth: {
|
|
1: [
|
|
{
|
|
depth: 1,
|
|
id: 'Function:src/svc.ts:applyDiscount',
|
|
name: 'applyDiscount',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
ambiguous: true,
|
|
processes: [],
|
|
},
|
|
{
|
|
depth: 1,
|
|
id: null,
|
|
name: '(top-level)',
|
|
type: 'BasicBlock',
|
|
filePath: 'src/svc.ts',
|
|
unresolved: true,
|
|
processes: [],
|
|
},
|
|
],
|
|
},
|
|
byDepthCounts: { 1: 2 },
|
|
}),
|
|
);
|
|
expect(out).toContain('2 block(s) could not be attributed');
|
|
expect(out).toContain('1 dependence block(s) map to no owning');
|
|
// The shadow / ambiguous rows carry their flags inline.
|
|
expect(out).toContain('[ambiguous]');
|
|
expect(out).toContain('[unresolved]');
|
|
});
|
|
|
|
it('flags truncation honestly', () => {
|
|
const out = formatImpactResult(pdgFindings({ truncated: true, truncatedBy: 'depth' }));
|
|
expect(out).toContain('Truncated');
|
|
expect(out).toContain('by depth');
|
|
expect(out).toContain('deeper PDG impacts may exist');
|
|
});
|
|
|
|
it('renders multiple truncation causes honestly', () => {
|
|
const out = formatImpactResult(
|
|
pdgFindings({
|
|
truncated: true,
|
|
truncatedBy: 'depth',
|
|
truncatedByReasons: ['depth', 'limit'],
|
|
}),
|
|
);
|
|
expect(out).toContain('Truncated');
|
|
expect(out).toContain('by depth, limit');
|
|
});
|
|
|
|
it('renders the degradation note as remediation, not a zero/empty blast radius', () => {
|
|
// Shaped like the `_impactImpl` pdgLayer-degradation early return.
|
|
const out = formatImpactResult({
|
|
mode: 'pdg',
|
|
pdgLayer: 'no-layer',
|
|
note: 'No PDG layer in this index. Run `gitnexus analyze --pdg` to build it.',
|
|
target: { name: 'computeTotal' },
|
|
direction: 'downstream',
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
});
|
|
// The remediation guidance is present.
|
|
expect(out).toContain('analyze --pdg');
|
|
expect(out).toContain('no usable PDG layer');
|
|
// It must NOT read as a confident "isolated / no dependencies / safe".
|
|
expect(out).not.toContain('isolated');
|
|
expect(out).not.toContain('No downstream dependencies found');
|
|
});
|
|
|
|
it('names the missing sub-layer in a partial-degradation note', () => {
|
|
const out = formatImpactResult({
|
|
mode: 'pdg',
|
|
pdgLayer: 'sub-layer-missing',
|
|
missingSubLayer: 'REACHING_DEF',
|
|
note: 'CDG present but REACHING_DEF absent.',
|
|
target: { name: 'computeTotal' },
|
|
direction: 'downstream',
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
});
|
|
expect(out).toContain('REACHING_DEF');
|
|
expect(out).toContain('analyze --pdg');
|
|
expect(out).not.toContain('isolated');
|
|
});
|
|
|
|
it('renders the no-body (KTD6) caveat, not "isolated / no dependencies"', () => {
|
|
// Shaped like `_runImpactPDG`'s no-body early return.
|
|
const out = formatImpactResult({
|
|
mode: 'pdg',
|
|
target: {
|
|
id: 'Interface:src/types.ts:Card',
|
|
name: 'Card',
|
|
type: 'Interface',
|
|
filePath: 'src/types.ts',
|
|
},
|
|
direction: 'downstream',
|
|
reachableBlocks: [],
|
|
blockCount: 0,
|
|
truncated: false,
|
|
depthReached: 0,
|
|
epistemic: 'no-pdg-body',
|
|
note:
|
|
"'Card' has no PDG body — no BasicBlocks / control- or data-dependence edges exist for " +
|
|
'this symbol (e.g. an interface, type alias, abstract/ambient member, or a one-line ' +
|
|
'declaration with no CFG). This is NOT a confident "no impact": the local PDG ' +
|
|
'statement slice cannot model this symbol kind. Inter-procedural symbol reach may still be attached.',
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
byDepth: {},
|
|
byDepthCounts: { 1: 0 },
|
|
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
|
affected_processes: [],
|
|
affected_modules: [],
|
|
unresolvedBlockCount: 0,
|
|
ambiguousProjectionCount: 0,
|
|
});
|
|
expect(out).toContain('no PDG body');
|
|
expect(out.toLowerCase()).toContain('not applicable');
|
|
// NOT the false-safe callgraph "isolated" headline. (The note may DISCLAIM
|
|
// "no impact", but the confident standalone "appears isolated." sentence
|
|
// and the callgraph "No ... dependencies found." headline must be absent.)
|
|
expect(out).not.toContain('appears isolated');
|
|
expect(out).not.toContain('No downstream dependencies found');
|
|
});
|
|
|
|
it('renders whole-symbol-empty as not-isolated, steering the caller to line:<N>', () => {
|
|
// `_runImpactPDG` reachableBlocks.length === 0 path WITHOUT a line (whole-
|
|
// symbol seed). The note now frames it as a structurally-empty WHOLE-SYMBOL
|
|
// slice and steers to `line:<N>` (the useful statement-anchored mode).
|
|
const out = formatImpactResult({
|
|
mode: 'pdg',
|
|
target: {
|
|
id: 'Function:src/svc.ts:noop',
|
|
name: 'noop',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
},
|
|
direction: 'downstream',
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
epistemic: 'pdg-intra-procedural',
|
|
note:
|
|
"'noop' has a PDG body but a WHOLE-SYMBOL downstream slice is empty: " +
|
|
'intra-procedural dependence stays inside the function, so every reachable block ' +
|
|
'is already part of the seed. Pass line:<N> to slice from a specific statement ' +
|
|
'(what depends on the code at that line). Inter-procedural symbol reach is attached ' +
|
|
'separately by the unified impact dispatcher.',
|
|
reachableBlocks: [],
|
|
blockCount: 0,
|
|
affectedStatements: [],
|
|
affectedStatementCount: 0,
|
|
depthReached: 1,
|
|
unresolvedBlockCount: 0,
|
|
ambiguousProjectionCount: 0,
|
|
byDepth: {},
|
|
byDepthCounts: { 1: 0 },
|
|
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
|
affected_processes: [],
|
|
affected_modules: [],
|
|
});
|
|
expect(out).toContain('no inter-procedural symbols reached');
|
|
// The new note steers to the statement-anchored mode.
|
|
expect(out).toContain('WHOLE-SYMBOL');
|
|
expect(out).toMatch(/line:<N>/);
|
|
// The caveat may reference the word "isolated" to disclaim it, but the
|
|
// confident callgraph "appears isolated." headline must be absent.
|
|
expect(out).not.toContain('appears isolated');
|
|
expect(out).not.toContain('No downstream dependencies found');
|
|
expect(out).toContain('Inter-procedural symbol reach is attached');
|
|
});
|
|
|
|
// ── Statement-anchored (mode:'pdg' + line) rendering ──────────────────────
|
|
// A representative statement-mode result, shaped like `assemblePdgImpactResult`
|
|
// emits when seeded on a line: criterionLine + affectedStatements + count.
|
|
function pdgStatementSlice(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
return {
|
|
mode: 'pdg',
|
|
target: {
|
|
id: 'Function:src/svc.ts:accum',
|
|
name: 'accum',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
},
|
|
direction: 'downstream',
|
|
criterionLine: 8,
|
|
affectedStatements: [
|
|
{ line: 10, filePath: 'src/svc.ts', text: 'sum = sum + x;' },
|
|
{ line: 12, filePath: 'src/svc.ts', text: 'return sum;' },
|
|
],
|
|
affectedStatementCount: 2,
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
epistemic: 'pdg-intra-procedural',
|
|
note:
|
|
"mode:'pdg' — intra-procedural slice from line 8 of 'accum'. 2 statements are " +
|
|
'downstream-dependent on it (over CDG + REACHING_DEF). Inter-procedural symbol reach ' +
|
|
'is attached separately by the unified impact dispatcher.',
|
|
reachableBlocks: ['b1', 'b2'],
|
|
blockCount: 2,
|
|
depthReached: 2,
|
|
unresolvedBlockCount: 0,
|
|
ambiguousProjectionCount: 0,
|
|
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
|
byDepthCounts: {},
|
|
affected_processes: [],
|
|
affected_modules: [],
|
|
byDepth: {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
it('renders a statement slice as an L<line>: <text> list under the criterion-line heading', () => {
|
|
const out = formatImpactResult(pdgStatementSlice());
|
|
// Heading carries direction + file:criterionLine + count.
|
|
expect(out).toContain('Statements downstream-dependent on src/svc.ts:8 (2):');
|
|
// Each dependent statement renders as ` L<line>: <text>`.
|
|
expect(out).toContain(' L10: sum = sum + x;');
|
|
expect(out).toContain(' L12: return sum;');
|
|
// It is the statement list; no inter-symbol section appears without byDepth reach.
|
|
expect(out).not.toContain('Inter-procedural symbol reach (');
|
|
// The unified PDG note still surfaces.
|
|
expect(out).toContain('Inter-procedural symbol reach is attached');
|
|
});
|
|
|
|
it('renders statement slices with inter-procedural symbol reach when present', () => {
|
|
const out = formatImpactResult(
|
|
pdgStatementSlice({
|
|
impactedCount: 1,
|
|
summary: { direct: 1, processes_affected: 0, modules_affected: 0 },
|
|
byDepthCounts: { 1: 1 },
|
|
byDepth: {
|
|
1: [
|
|
{
|
|
depth: 1,
|
|
id: 'Function:src/caller.ts:caller',
|
|
name: 'caller',
|
|
type: 'Function',
|
|
filePath: 'src/caller.ts',
|
|
processes: [],
|
|
},
|
|
],
|
|
},
|
|
pdgInterprocedural: { engine: 'symbol-graph', impactedCount: 1, byDepthCounts: { 1: 1 } },
|
|
}),
|
|
);
|
|
|
|
expect(out).toContain('Statements downstream-dependent on src/svc.ts:8 (2):');
|
|
expect(out).toContain('Inter-procedural symbol reach (1):');
|
|
expect(out).toContain('Function caller → src/caller.ts');
|
|
});
|
|
|
|
it('flags slice truncation honestly', () => {
|
|
const out = formatImpactResult(pdgStatementSlice({ truncated: true, truncatedBy: 'depth' }));
|
|
expect(out).toContain('Truncated');
|
|
expect(out).toContain('by depth');
|
|
});
|
|
|
|
it('flags truncated empty statement slices honestly', () => {
|
|
const out = formatImpactResult(
|
|
pdgStatementSlice({
|
|
affectedStatements: [],
|
|
affectedStatementCount: 0,
|
|
truncated: true,
|
|
truncatedBy: 'limit',
|
|
note: 'Statement slice stopped at the configured result limit.',
|
|
}),
|
|
);
|
|
|
|
expect(out).toContain('No statements downstream-dependent on src/svc.ts:8');
|
|
expect(out).toContain('Truncated');
|
|
expect(out).toContain('by limit');
|
|
expect(out).toContain('Statement slice stopped at the configured result limit.');
|
|
});
|
|
|
|
it('renders a no-block-at-line result as the steering note, never an empty isolated headline', () => {
|
|
// `_runImpactPDG` seedBlocks.length === 0 in statement mode.
|
|
const out = formatImpactResult({
|
|
mode: 'pdg',
|
|
target: {
|
|
id: 'Function:src/svc.ts:accum',
|
|
name: 'accum',
|
|
type: 'Function',
|
|
filePath: 'src/svc.ts',
|
|
},
|
|
direction: 'downstream',
|
|
criterionLine: 9,
|
|
reachableBlocks: [],
|
|
blockCount: 0,
|
|
affectedStatements: [],
|
|
affectedStatementCount: 0,
|
|
truncated: false,
|
|
depthReached: 0,
|
|
epistemic: 'pdg-no-block-at-line',
|
|
note:
|
|
"No PDG statement block starts at line 9 within 'accum' (src/svc.ts). The line may be " +
|
|
"blank, a comment, a brace, or outside the symbol's body. Pass a line that begins an " +
|
|
'executable statement.',
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
byDepth: {},
|
|
byDepthCounts: { 1: 0 },
|
|
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
|
affected_processes: [],
|
|
affected_modules: [],
|
|
unresolvedBlockCount: 0,
|
|
ambiguousProjectionCount: 0,
|
|
});
|
|
expect(out).toContain('No statements downstream-dependent on src/svc.ts:9');
|
|
expect(out).toContain('No PDG statement block starts at line 9');
|
|
expect(out).not.toContain('appears isolated');
|
|
expect(out).not.toContain('PDG-dependent symbols');
|
|
});
|
|
|
|
it('suppresses callgraph next-step hints for PDG and failed impact results', () => {
|
|
expect(getNextStepHint('impact')).toContain('Review d=1 items first');
|
|
expect(getNextStepHint('impact', pdgStatementSlice())).toBe('');
|
|
expect(getNextStepHint('impact', { mode: 'pdg', pdgLayer: 'no-layer' })).toBe('');
|
|
expect(getNextStepHint('impact', { mode: 'pdg', status: 'ambiguous' })).toBe('');
|
|
expect(getNextStepHint('impact', { error: 'Target not found' })).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('formatImpactResult — callgraph rendering is UNCHANGED (regression guard)', () => {
|
|
// A known callgraph result. The exact rendered string is pinned: U5 must not
|
|
// perturb the default-mode output by one byte (KTD8 interchangeability).
|
|
const callgraphResult = {
|
|
target: { kind: 'Function', name: 'computeTotal' },
|
|
direction: 'upstream',
|
|
impactedCount: 2,
|
|
risk: 'MEDIUM',
|
|
byDepthCounts: { 1: 1, 2: 1 },
|
|
byDepth: {
|
|
1: [
|
|
{
|
|
type: 'Function',
|
|
name: 'callerA',
|
|
filePath: 'src/a.ts',
|
|
relationType: 'CALLS',
|
|
confidence: 1,
|
|
},
|
|
],
|
|
2: [
|
|
{
|
|
type: 'Function',
|
|
name: 'callerB',
|
|
filePath: 'src/b.ts',
|
|
relationType: 'CALLS',
|
|
confidence: 0.8,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
it('renders the callgraph result with Risk on the callgraph contract (byte-identical for U5+risk)', () => {
|
|
const expected = [
|
|
'Blast radius for Function computeTotal (upstream): 2 symbol(s) depends on this (will break if changed)',
|
|
'Risk: MEDIUM',
|
|
'',
|
|
'd=1: WILL BREAK (direct) (1)',
|
|
' Function callerA → src/a.ts [CALLS]',
|
|
'',
|
|
'd=2: LIKELY AFFECTED (indirect) (1)',
|
|
' Function callerB → src/b.ts [CALLS] (conf: 0.8)',
|
|
].join('\n');
|
|
expect(formatImpactResult(callgraphResult)).toBe(expected);
|
|
});
|
|
|
|
it('does not apply any PDG framing to a callgraph result', () => {
|
|
const out = formatImpactResult(callgraphResult);
|
|
expect(out).not.toContain('PDG-dependent symbols');
|
|
expect(out).not.toContain('intra-procedural');
|
|
expect(out).not.toContain('analyze --pdg');
|
|
});
|
|
|
|
it('renders the callgraph summary-only branch unchanged', () => {
|
|
const out = formatImpactResult({
|
|
target: { kind: 'Function', name: 'foo' },
|
|
direction: 'downstream',
|
|
impactedCount: 3,
|
|
risk: 'LOW',
|
|
byDepthCounts: { 1: 2, 2: 1 },
|
|
// no byDepth → summary-only branch
|
|
});
|
|
expect(out).toContain('(summary only — use summaryOnly: false to see symbol lists)');
|
|
expect(out).toContain('d=1: WILL BREAK (direct) (2)');
|
|
expect(out).not.toContain('PDG-dependent symbols');
|
|
});
|
|
|
|
it('renders the callgraph isolated / zero case with risk, without claiming isolation', () => {
|
|
const out = formatImpactResult({
|
|
target: { name: 'lonely' },
|
|
direction: 'downstream',
|
|
impactedCount: 0,
|
|
risk: 'LOW',
|
|
});
|
|
expect(out).toBe('lonely: No downstream dependencies found.\nRisk: LOW');
|
|
});
|
|
|
|
it('renders the callgraph lower-bound (DI/dynamic-dispatch) copy unchanged', () => {
|
|
const out = formatImpactResult({
|
|
target: { name: 'viaInterface' },
|
|
direction: 'upstream',
|
|
impactedCount: 0,
|
|
risk: 'UNKNOWN',
|
|
epistemic: 'lower-bound',
|
|
boundaries: ['interface PaymentGateway'],
|
|
});
|
|
expect(out).toContain('LOWER BOUND');
|
|
expect(out).toContain('interface PaymentGateway');
|
|
expect(out).not.toContain('PDG');
|
|
});
|
|
});
|