mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
* fix(lbug): store exact symbol content snippets * fix(ingestion): emit 0-based line numbers for COBOL/JCL/scope/markdown nodes COBOL/JCL processors, the scope-graph emitter, and the markdown Section emitter stored 1-based startLine/endLine, unlike every tree-sitter node (0-based). The exact-content slice (#2379) then dropped each symbol's declaration line for those languages. Convert to 0-based at the graph-node emission boundary via toZeroBasedLine — leaving parser-internal .line values, L${line} node/edge IDs, and containment checks untouched. Refs #2377, #2379 * refactor(lbug): single source of truth for symbol-content labels Extract SYMBOL_NODE_LABELS so the exact-content label set can't drift the way the inline copy did in #2379. csv-generator derives EXACT_SYMBOL_CONTENT_LABELS from it; manifest-extractor's near-identical allowlist is left behavior-unchanged (intentional subset, #2325-test-locked) with a documented cross-reference. Refs #2379 * test(ingestion): cover 0-based emitter output and pin exact-content slicing - csv-pipeline: replace the blank-buffer fixture (a +/-1 shift silently passed) with directly-adjacent neighbors; add one-line-symbol and Section (+/-2 fallback) cases. - cobol resolver: assert COBOL Module and JCL job/step emit 0-based startLine. - markdown CRLF: update Section startLine/endLine expectations to 0-based. Refs #2377, #2379 * feat(mcp): present 1-based line numbers in context/query/impact tools GraphNode startLine/endLine are stored 0-based (tree-sitter rows), which surprised users querying them (they don't line up with editors/sed). Add toDisplayLine and apply it at the context/query/impact response boundaries so line numbers are editor/sed-aligned. Raw cypher stays 0-based (documented in the schema resource); BasicBlock/PDG statement lines (already 1-based) and internal join params are left untouched. Refs #2377 * test(mcp): assert 1-based tool exposure with raw cypher staying 0-based context() reports startLine+1 (editor/sed aligned); a raw cypher RETURN of the same node keeps the stored 0-based value. Guards against double-conversion and leaking the display shift into raw results. Refs #2377 * fix(mcp): stop query() double-converting BM25 line numbers bm25Search applied toDisplayLine to its result rows, and query()'s aggregation loop applied it again, so BM25-matched symbols reported lines shifted +2 (stored 0-based 41 read as 43, not 42) while semantic-matched symbols were correct. bm25Search is called only from query(); return raw 0-based rows and let the single aggregation-loop conversion handle both retrievers. Adds a query() BM25 regression test asserting stored 41 -> 42 (would be 43 if double-converted), which the prior mcp-line-display test — covering only context()+cypher — never exercised. (#2380, #2377) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): use ?? not || so first-line symbols keep their line number `sym.startLine || sym[4]` treated a legitimate 0-based startLine of 0 as absent, so context()/query() dropped startLine/endLine for every symbol on line 1 of its file — every COBOL Module (toZeroBasedLine(1) = 0) and markdown h1. `??` only falls through to the positional fallback on null/undefined, preserving a real 0. This also repairs the rename definition-edit path, which consumes context()'s value. Adds a context() first-line (startLine:0 -> 1) assertion. (#2380, #2377) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): make group/cross-repo trace line numbers 1-based consistently A group/cross-repo trace presented 1-based endpoints (via resolveSymbolForGroup) but 0-based hops (tagHops copies port.trace output verbatim), so one response mixed bases. Wrap the trace port adapter (traceForGroup) to convert hop lines to 1-based too, matching the endpoints. Single-repo trace dispatches directly (not through this port) and stays 0-based — full single-repo parity is a tracked follow-up. core/group stays display-agnostic (no mcp import). Extends the cross-trace e2e test to assert hops share the endpoints' base (checkout 10 -> 11, getUsers 1 -> 2). (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): present explain/pdg_query anchor line 1-based resolveBlockAnchor converted its ambiguous-candidate lines to 1-based but left the resolved-target anchor raw 0-based, so the same tool reported two bases depending on whether the target was ambiguous. Convert the display anchor to 1-based via toDisplayLine. The BasicBlock join param (symStart: sym.startLine + 1) is untouched — it targets the 1-based BasicBlock id space, not display. Asserts the resolved anchor is 1-based (targetFn stored 10 -> 11). (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): bump schema + PDG result versions for the line-number change The 0-based storage flip for COBOL/JCL/markdown/scope (#2377/#2379) changed on-disk line semantics, and the PDG result startLine is now 1-based (#2380). Neither shipped a version bump, so an incremental re-analyze would preserve old 1-based rows (mixed-base index rendered one line too high) and PDG consumers got no signal. - INCREMENTAL_SCHEMA_VERSION 5 -> 6 (forces a one-time full re-analyze) - PDG_RESULT_VERSION 1 -> 2 (result-shape discriminator) Updates the version-pinning tests, the pdgResultVersion result type, and the tools.ts PDG output-contract doc. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(group): guard manifest label list against SYMBOL_NODE_LABELS drift manifest-extractor's CUSTOM_CONTRACT_RESOLVE_QUERY hand-lists the contract-resolvable labels as a deliberate subset of the shared SYMBOL_NODE_LABELS, guarded only by a comment — the same drift class (#2379) the shared-set refactor eliminated elsewhere. Derive the query's label set and assert it is a strict subset whose difference is exactly {Namespace, Variable, Module}, so adding a symbol label without a conscious manifest decision fails. Query string stays literal (#2325-test-locked). (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(mcp): document which tools present 1-based vs 0-based line numbers The schema-resource note listed only context/query/impact as 1-based. After the trace/anchor fixes it now enumerates the full set — context, query, impact, group/cross-repo trace, and explain/pdg_query anchors are 1-based; raw Cypher and single-repo trace stay 0-based (full single-repo-trace parity is a tracked follow-up); BasicBlock/PDG statement lines are separately 1-based. (#2377, #2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): pin impact() line-value display (close the coverage gap) The prior mcp-line-display test only asserted context() + raw cypher, which is why the query() double-conversion (#2380) shipped green. Adds an impact() line-value assertion via the ambiguous-candidate path (the only impact response that surfaces a per-candidate line): two same-name symbols force ambiguity and the candidate at stored 0-based 41 must read 42. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): fix stale rename #2283 mock after 1-based context display rename resolves its symbol via context(), which now presents startLine 1-based (#2377), then subtracts 1 to recover the 0-based file index. The #2283 mock stored startLine:1 but put `oldName` on the file's line 0, so after the 1-based shift the definition edit no longer matched and the write-failure path never fired — the test read 'success' instead of 'partial'. Align the mock content to its stored line (oldName on 0-based line 1). Pre-existing failure surfaced once ubuntu/coverage completed on this branch. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): consolidate line-display tests into one shared DB block The query()/BM25 case had spun up a second full LadybugDB + FTS setup; fold it into the single existing block (adding FTS + the Zqxwvbm seed there) so the file builds one DB, not two. Trims per-file setup cost — relevant to the Windows platform-sensitive suite's under-load 15-minute timeout. Same five assertions, all green. (#2380) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: kigland <shuaizhicheng336@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
558 lines
20 KiB
TypeScript
558 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: 2,
|
|
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: 2 });
|
|
});
|
|
|
|
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 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');
|
|
});
|
|
});
|