diff --git a/gitnexus/bench/impact-pdg/measure.mjs b/gitnexus/bench/impact-pdg/measure.mjs index a34cde84b..1e872223f 100644 --- a/gitnexus/bench/impact-pdg/measure.mjs +++ b/gitnexus/bench/impact-pdg/measure.mjs @@ -559,6 +559,9 @@ async function run() { skipped: pdgRes.pdgLayer !== undefined && pdgRes.pdgLayer !== 'ready', }; } finally { + const lbugPath = path.join(work, '.gitnexus', 'lbug'); + await closeLbug(lbugPath).catch(() => {}); + initialised.delete(lbugPath); fs.rmSync(work, { recursive: true, force: true }); } } diff --git a/gitnexus/src/cli/eval-server.ts b/gitnexus/src/cli/eval-server.ts index a722ce44e..b7be1bb08 100644 --- a/gitnexus/src/cli/eval-server.ts +++ b/gitnexus/src/cli/eval-server.ts @@ -324,10 +324,17 @@ export function formatImpactResult(result: any): string { // No statement block at the line, or no dependents in this direction. // Print the honest note (pdg-no-block-at-line or the no-dependence note) // verbatim — never an empty "isolated" headline. - return ( - `No statements ${direction}-dependent on ${anchorFile}:${result.criterionLine}.` + - (result.note ? `\n${result.note}` : '') - ); + const emptySliceLines = [ + `No statements ${direction}-dependent on ${anchorFile}:${result.criterionLine}.`, + ]; + if (result.truncated) { + const by = formatTruncationSuffix(result); + emptySliceLines.push( + `⚠️ Truncated${by} — the dependence slice was bounded; deeper PDG-dependent statements may exist.`, + ); + } + if (result.note) emptySliceLines.push(result.note); + return emptySliceLines.join('\n'); } const slLines: string[] = []; @@ -587,7 +594,7 @@ function formatToolResult(toolName: string, result: any): string { // Guide the agent to the logical next tool call. // Critical for tool chaining: query → context → impact → fix. -function getNextStepHint(toolName: string): string { +export function getNextStepHint(toolName: string, result?: any): string { switch (toolName) { case 'query': return '\n---\nNext: Pick a symbol above and run gitnexus-context "" to see all its callers, callees, and execution flows.'; @@ -596,6 +603,15 @@ function getNextStepHint(toolName: string): string { return '\n---\nNext: To check what breaks if you change this, run gitnexus-impact "" upstream'; case 'impact': + if ( + result?.error || + result?.status === 'ambiguous' || + result?.mode === 'pdg' || + result?.pdgLayer || + typeof result?.criterionLine === 'number' + ) { + return ''; + } return '\n---\nNext: Review d=1 items first (WILL BREAK). Read the source with cat to understand the code, then make your fix.'; case 'cypher': @@ -711,7 +727,7 @@ export async function evalServerCommand(options?: EvalServerOptions): Promise 0; - } catch { + } catch (err) { // db-lock / missing-path / corrupt probe — fall through as not-visible, but - // keep the `'unknown'` signal (a probe failure must not lose it). + // keep the `'unknown'` signal AND preserve the probe failure. Reporting the + // failed probe as "no edges visible" hides a DB-health problem from operators. + probeError = err instanceof Error ? err.message : String(err); edgesVisible = false; } + if (probeError) { + return { + state: 'unknown', + probeError, + recoverySuggestion: + 'Check for a LadybugDB lock/corruption or missing index path. Stop overlapping GitNexus processes, retry, or re-run gitnexus analyze --pdg.', + note: + `PDG layer status unknown — CDG/REACHING_DEF probe failed: ${probeError}. ` + + `The layer cannot be confirmed complete; this is distinct from "no edges visible".`, + }; + } return { state: 'unknown', note: edgesVisible diff --git a/gitnexus/test/unit/cli-impact-pdg-format.test.ts b/gitnexus/test/unit/cli-impact-pdg-format.test.ts index 15c7909b9..828e8519f 100644 --- a/gitnexus/test/unit/cli-impact-pdg-format.test.ts +++ b/gitnexus/test/unit/cli-impact-pdg-format.test.ts @@ -14,7 +14,7 @@ * rendering stays byte-identical (regression guard). */ import { describe, expect, it } from 'vitest'; -import { formatImpactResult } from '../../src/cli/eval-server.js'; +import { formatImpactResult, getNextStepHint } from '../../src/cli/eval-server.js'; // A representative PDG findings result, shaped exactly like // `assemblePdgImpactResult` (pdg-impact.ts) emits. @@ -370,6 +370,23 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => { 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({ @@ -408,6 +425,14 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => { 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)', () => { diff --git a/gitnexus/test/unit/pdg-impact-engine.test.ts b/gitnexus/test/unit/pdg-impact-engine.test.ts index f4e60ab2a..9899ecfaa 100644 --- a/gitnexus/test/unit/pdg-impact-engine.test.ts +++ b/gitnexus/test/unit/pdg-impact-engine.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { IMPACT_MAX_DEPTH } from '../../src/mcp/tools.js'; -import { runImpactPDG } from '../../src/mcp/local/pdg-impact.js'; +import { pdgLayerStatus, runImpactPDG } from '../../src/mcp/local/pdg-impact.js'; describe('runImpactPDG', () => { it('clamps huge maxDepth values to the documented impact traversal cap', async () => { @@ -74,3 +74,50 @@ describe('runImpactPDG', () => { expect((result as any).affectedStatements.map((s: any) => s.text)).toEqual(['a();', 'b();']); }); }); + +describe('pdgLayerStatus', () => { + const unreadableMeta = async () => null as any; + + it('reports visible PDG edges as unknown without a probe error when meta is unreadable', async () => { + const result = await pdgLayerStatus({ + lbugPath: 'repo/.gitnexus/lbug', + loadMetaFn: unreadableMeta, + executeParameterized: (async (_repo: string, query: string) => { + expect(query).toContain('LIMIT 1'); + return [{ type: 'CDG' }]; + }) as any, + }); + + expect(result.state).toBe('unknown'); + expect(result.note).toContain('edges ARE visible'); + expect(result.probeError).toBeUndefined(); + }); + + it('reports no visible PDG edges separately from probe failures', async () => { + const result = await pdgLayerStatus({ + lbugPath: 'repo/.gitnexus/lbug', + loadMetaFn: unreadableMeta, + executeParameterized: (async () => []) as any, + }); + + expect(result.state).toBe('unknown'); + expect(result.note).toContain('no CDG/REACHING_DEF edges visible'); + expect(result.probeError).toBeUndefined(); + }); + + it('preserves probe failures instead of reporting a false no-edge signal', async () => { + const result = await pdgLayerStatus({ + lbugPath: 'repo/.gitnexus/lbug', + loadMetaFn: unreadableMeta, + executeParameterized: (async () => { + throw new Error('database busy'); + }) as any, + }); + + expect(result.state).toBe('unknown'); + expect(result.probeError).toBe('database busy'); + expect(result.note).toContain('probe failed'); + expect(result.note).not.toContain('no CDG/REACHING_DEF edges visible'); + expect(result.recoverySuggestion).toContain('LadybugDB'); + }); +});