mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
feat(impact): CLI rendering for PDG results (U5)
formatImpactResult gains a PDG-aware branch (detected on mode:'pdg') that returns before any callgraph path: renders findings as 'PDG-dependent symbols' (not 'depth N'), prints the intra-procedural caveat, surfaces ambiguous/unresolved/truncation honestly, routes the degradation note to 'analyze --pdg' guidance and the no-body note to a KTD6 caveat (never 'isolated'). callgraph rendering byte-identical. ai-context adds a hasPdg-gated --mode pdg hint. Refs U5
This commit is contained in:
parent
3d12eab6f9
commit
f63f3ebeac
3 changed files with 460 additions and 1 deletions
|
|
@ -199,7 +199,11 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
|
|||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.${
|
||||
hasPdg
|
||||
? ` For finer, intra-procedural precision within a function, add \`mode: "pdg"\` — it traces control/data dependence (CDG + REACHING_DEF) instead of call-graph reachability, but does NOT model cross-function impact (\`--pdg\` layer).`
|
||||
: ''
|
||||
}
|
||||
- **MUST run \`detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use \`query({search_query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
|
|
|
|||
|
|
@ -219,6 +219,128 @@ export function formatImpactResult(result: any): string {
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ─── PDG mode (mode:'pdg') ────────────────────────────────────────────
|
||||
// KTD8 presentation half. PDG results are intra-procedural Program
|
||||
// Dependence Graph blast radii: the single collapsed `byDepth[1]` bucket
|
||||
// has NO call-hop depth meaning (block-hops ≠ call-hops), so we must NOT
|
||||
// reuse the callgraph "depth N / WILL BREAK (direct)" framing, the
|
||||
// callgraph DI/dynamic-dispatch lower-bound copy, or the confident
|
||||
// "isolated" zero. A degraded / no-body PDG result is INCONCLUSIVE, not
|
||||
// safe-to-refactor — it gets the explicit caveat + remediation, never an
|
||||
// empty blast radius. Detect on `mode:'pdg'` (every PDG return path —
|
||||
// findings, degradation, no-body, no-dependence — carries it). Ambiguous
|
||||
// PDG results carry `status:'ambiguous'` and are handled above; they never
|
||||
// reach here.
|
||||
if (result.mode === 'pdg') {
|
||||
const name = target?.name || '?';
|
||||
|
||||
// (1) Degradation — the PDG layer (or a sub-layer) is absent/unreadable.
|
||||
// `pdgLayer` is the non-'ready' state from `pdgLayerStatus`. Print the
|
||||
// honest remediation, NOT a zero/empty blast radius.
|
||||
if (result.pdgLayer) {
|
||||
const subLayer = result.missingSubLayer
|
||||
? ` (missing sub-layer: ${result.missingSubLayer})`
|
||||
: '';
|
||||
return (
|
||||
`${name}: PDG impact unavailable — the index has no usable PDG layer ` +
|
||||
`[${result.pdgLayer}]${subLayer}. This is NOT "no impact". ` +
|
||||
`Re-index with \`gitnexus analyze --pdg\` to build the control/data ` +
|
||||
`dependence layer, or use \`--mode callgraph\` for the call-graph blast radius.` +
|
||||
(result.note ? `\n${result.note}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
// (2) No-body symbol (KTD6) — interface / type alias / abstract / ambient
|
||||
// member / one-line declaration with no CFG. Show the caveat, never
|
||||
// "isolated / no dependencies".
|
||||
if (result.epistemic === 'no-pdg-body') {
|
||||
return (
|
||||
`${name}: PDG mode not applicable to this symbol — it has no PDG body ` +
|
||||
`(no control/data dependence edges; e.g. an interface, type alias, ` +
|
||||
`abstract/ambient member, or a one-line declaration). This is NOT a ` +
|
||||
`confident "no impact". Use \`--mode callgraph\` for its inter-procedural ` +
|
||||
`blast radius.` +
|
||||
(result.note ? `\n${result.note}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
const items: any[] = (result.byDepth && result.byDepth[1]) || [];
|
||||
const bucketCount = result.byDepthCounts?.[1] ?? items.length;
|
||||
const pdgLines: string[] = [];
|
||||
|
||||
// (3) Has a body but no intra-procedural dependence reachability.
|
||||
// `impactedCount === 0` with no findings — still NOT "isolated": the count
|
||||
// is a per-function lower bound, and inter-procedural impact is unmodeled.
|
||||
if (total === 0 && bucketCount === 0) {
|
||||
pdgLines.push(
|
||||
`${name} (${direction}): no intra-procedural PDG-dependent symbols found. ` +
|
||||
`This is NOT a confident "isolated / no dependencies" — cross-function ` +
|
||||
`(inter-procedural) impact is not modeled in PDG mode. Use \`--mode callgraph\` ` +
|
||||
`for the call-graph blast radius.`,
|
||||
);
|
||||
} else {
|
||||
// (4) Findings — render the collapsed bucket under a "PDG-dependent
|
||||
// symbols" heading (NOT "depth N"). `total` (impactedCount) is distinct
|
||||
// owning SYMBOLS; `bucketCount` includes any `unresolved` shadow rows.
|
||||
const dirLabel =
|
||||
direction === 'upstream'
|
||||
? 'this depends on (intra-procedural)'
|
||||
: 'depend on this (intra-procedural)';
|
||||
pdgLines.push(
|
||||
`PDG-dependent symbols for ${target?.kind || ''} ${name} (${direction}): ` +
|
||||
`${total} symbol(s) ${dirLabel}`,
|
||||
);
|
||||
pdgLines.push('');
|
||||
const shown = Math.min(items.length, 12);
|
||||
for (const item of items.slice(0, shown)) {
|
||||
const flags: string[] = [];
|
||||
if (item.unresolved) flags.push('unresolved');
|
||||
if (item.ambiguous) flags.push('ambiguous');
|
||||
const flagStr = flags.length ? ` [${flags.join(', ')}]` : '';
|
||||
pdgLines.push(` ${item.type || ''} ${item.name} → ${item.filePath}${flagStr}`);
|
||||
}
|
||||
if (bucketCount > shown) {
|
||||
pdgLines.push(` ... and ${bucketCount - shown} more`);
|
||||
}
|
||||
}
|
||||
|
||||
// Intra-procedural caveat — always present for a non-degraded PDG result.
|
||||
// The assembled `note` already carries the cross-function caveat + the
|
||||
// ambiguous/unresolved breakdown; surface it verbatim so the CLI reader
|
||||
// sees the same honesty the JSON consumer does.
|
||||
if (result.note) {
|
||||
pdgLines.push('');
|
||||
pdgLines.push(`ℹ️ ${result.note}`);
|
||||
} else {
|
||||
pdgLines.push('');
|
||||
pdgLines.push(
|
||||
"ℹ️ Intra-procedural Program Dependence Graph — cross-function impact is not modeled in this mode.",
|
||||
);
|
||||
}
|
||||
|
||||
// Honest incompleteness signals (block-attribution + truncation).
|
||||
if (result.ambiguousProjectionCount > 0) {
|
||||
pdgLines.push(
|
||||
`⚠️ ${result.ambiguousProjectionCount} block(s) could not be attributed to a ` +
|
||||
`unique owning symbol (same-line functions) — all colliding symbols are shown.`,
|
||||
);
|
||||
}
|
||||
if (result.unresolvedBlockCount > 0) {
|
||||
pdgLines.push(
|
||||
`⚠️ ${result.unresolvedBlockCount} dependence block(s) map to no owning ` +
|
||||
`Function/Method (top-level statement / closure) — surfaced under their file.`,
|
||||
);
|
||||
}
|
||||
if (result.truncated) {
|
||||
const by = result.truncatedBy ? ` (by ${result.truncatedBy})` : '';
|
||||
pdgLines.push(
|
||||
`⚠️ Truncated${by} — the dependence traversal was bounded; deeper PDG impacts may exist.`,
|
||||
);
|
||||
}
|
||||
|
||||
return pdgLines.join('\n').trim();
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
// #1858 — "isolated" is a confident claim. If an interface / indirection
|
||||
// boundary is on the path, the true count is a lower bound, not zero;
|
||||
|
|
|
|||
333
gitnexus/test/unit/cli-impact-pdg-format.test.ts
Normal file
333
gitnexus/test/unit/cli-impact-pdg-format.test.ts
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
/**
|
||||
* U5 — CLI / consumer rendering for PDG (`mode:'pdg'`) impact results.
|
||||
*
|
||||
* Guards the KTD8 presentation contract: PDG results must render HONESTLY —
|
||||
* - findings under a "PDG-dependent symbols" heading, NOT "depth N"
|
||||
* (block-hops are not call-hops);
|
||||
* - the intra-procedural caveat ("cross-function impact not modeled");
|
||||
* - 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 } from '../../src/cli/eval-server.js';
|
||||
|
||||
// A representative PDG findings result, shaped exactly like
|
||||
// `assemblePdgImpactResult` (local-backend.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',
|
||||
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). Cross-function (inter-procedural) ' +
|
||||
"impact is NOT modeled in this mode — use mode:'callgraph' for the call-graph blast radius.",
|
||||
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 findings under PDG-dependent framing, not "depth N"', () => {
|
||||
const out = formatImpactResult(pdgFindings());
|
||||
|
||||
// PDG framing — NOT the callgraph "depth N / WILL BREAK (direct)" labels.
|
||||
expect(out).toContain('PDG-dependent symbols');
|
||||
expect(out).not.toMatch(/d=\d/);
|
||||
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 intra-procedural caveat is present (cross-function not modeled).
|
||||
expect(out.toLowerCase()).toContain('cross-function');
|
||||
expect(out.toLowerCase()).toContain('not modeled');
|
||||
|
||||
// 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('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 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 intra-procedural PDG ' +
|
||||
"mode cannot model this symbol kind. Use mode:'callgraph' for its inter-procedural blast radius.",
|
||||
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 has-body-but-no-dependence as not-isolated, with the cross-function caveat', () => {
|
||||
// `_runImpactPDG` reachableBlocks.length === 0 path (body exists, no edges).
|
||||
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 no intra-procedural downstream dependence edges " +
|
||||
'(no CDG/REACHING_DEF reachability from its blocks). This is distinct from "no PDG body". ' +
|
||||
"Use mode:'callgraph' for its inter-procedural blast radius.",
|
||||
reachableBlocks: [],
|
||||
blockCount: 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 intra-procedural PDG-dependent symbols');
|
||||
// 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.toLowerCase()).toContain('cross-function');
|
||||
});
|
||||
});
|
||||
|
||||
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 the exact pre-U5 text (byte-identical)', () => {
|
||||
const expected = [
|
||||
'Blast radius for Function computeTotal (upstream): 2 symbol(s) depends on this (will break if changed)',
|
||||
'',
|
||||
'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 unchanged', () => {
|
||||
const out = formatImpactResult({
|
||||
target: { name: 'lonely' },
|
||||
direction: 'downstream',
|
||||
impactedCount: 0,
|
||||
risk: 'LOW',
|
||||
});
|
||||
expect(out).toBe('lonely: No downstream dependencies found. This symbol appears isolated.');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue