From c39ac409e5f896ba2024e298b9aa5daf4be9acdf Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Mon, 6 Jul 2026 05:01:25 -0600 Subject: [PATCH 1/2] refactor: unify impact-level rendering format between audit and fn-impact commands `renderAuditFunction` (presentation/audit.ts) and `printFnImpactLevels` (presentation/queries-cli/impact.ts) both rendered the same `Record` transitive-caller-levels shape (produced by the shared `bfsTransitiveCallers` BFS) but with two different, independently-maintained text formats. Extract a single `renderImpactLevels(levels, opts)` helper into a new presentation/impact-levels.ts module and adopt it in both call sites, using the richer icon+truncation format from fn-impact as the canonical one (per issue #1756's recommendation). `opts.emptyMessage` lets audit.ts suppress the "No callers found." line, since its "Impact: N transitive dependent(s)" line already conveys a zero count and none of its other subsections (Calls/Called by/Tests) print an explicit empty message either. Investigated downstream dependents before making this change: - No test asserts on audit.ts's exact text output; tests/integration/audit.test.ts only exercises the auditData data layer, and all CLI-level audit/fn-impact tests (tests/integration/cli.test.ts, tests/unit/queries-unit.test.ts) use --json. - No skill or hook in .claude/ parses this text; every `codegraph audit` invocation across .claude/skills/ already passes --json. - docs/examples/CLI.md and MCP.md do document an audit output example, but it was already stale relative to the current implementation independent of this change (wrong header/complexity/impact wording, single-line "Calls:" list) -- filed as optave/ops-codegraph-tool#1873 rather than fixing inline. - README.md/CLAUDE.md/ROADMAP.md contain no example output text for audit's impact-level rendering (checked directly) -- docs check acknowledged. BEHAVIOR CHANGE: `codegraph audit`'s impact-level text output now matches `codegraph fn-impact`'s icon+truncation format (per-level header with count, `^ :` per entry, truncated at 20 with "... and N more") instead of the old plain `Level {n}: name1, name2, ...` comma list. --json/--ndjson output is unaffected. Fixes #1756 Impact: 9 functions changed, 6 affected --- src/presentation/audit.ts | 9 ++-- src/presentation/impact-levels.ts | 60 ++++++++++++++++++++++++++ src/presentation/queries-cli/impact.ts | 17 +------- 3 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 src/presentation/impact-levels.ts diff --git a/src/presentation/audit.ts b/src/presentation/audit.ts index 9a350a6e7..2670b7696 100644 --- a/src/presentation/audit.ts +++ b/src/presentation/audit.ts @@ -2,6 +2,7 @@ import { kindIcon } from '../domain/queries.js'; import { auditData } from '../features/audit.js'; import { outputResult } from '../infrastructure/result-formatter.js'; import type { AuditFunctionEntry, AuditResult, CodegraphConfig } from '../types.js'; +import { renderImpactLevels } from './impact-levels.js'; interface AuditOpts { json?: boolean; @@ -62,12 +63,12 @@ function renderThresholdBreaches(fn: AuditFunctionEntry): void { } } -/** Render the transitive-dependent impact summary, one line per BFS level. */ +/** Render the transitive-dependent impact summary, one block per BFS level. */ function renderImpactSection(fn: AuditFunctionEntry): void { console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`); - for (const [level, nodes] of Object.entries(fn.impact.levels)) { - console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`); - } + // No "0 found" message here -- the count above already conveys it, matching this + // file's other sections (e.g. renderCallRefs), which print nothing when empty. + renderImpactLevels(fn.impact.levels, { emptyMessage: null }); } /** Render a labeled list of call references (used for both "Calls" and "Called by"). */ diff --git a/src/presentation/impact-levels.ts b/src/presentation/impact-levels.ts new file mode 100644 index 000000000..ce8262ce7 --- /dev/null +++ b/src/presentation/impact-levels.ts @@ -0,0 +1,60 @@ +/** + * Shared renderer for transitive-caller "impact levels" — the `{ level -> entries[] }` + * map produced by `bfsTransitiveCallers` (see domain/analysis/fn-impact.js). + * + * Both `codegraph audit` (presentation/audit.js) and `codegraph fn-impact` + * (presentation/queries-cli/impact.js) render this same shape and must stay + * visually identical — this module is the single source of truth for that format. + */ + +import { kindIcon } from '../domain/queries.js'; + +/** A single transitive-caller entry within one impact-level bucket. */ +export interface ImpactLevelRef { + name: string; + kind: string; + file: string; + line: number; +} + +export interface RenderImpactLevelsOpts { + /** + * Message printed when there are no levels at all. Pass `null` to print nothing + * (e.g. when the caller already reports a "0" count immediately above). + * Default: " No callers found." + */ + emptyMessage?: string | null; + /** Max entries shown per level before truncating with "... and N more". Default: 20. */ + limit?: number; +} + +/** + * Render a transitive-caller level map as indented, icon-prefixed bullet lists — + * one block per BFS level, with deeper levels indented further: + * + * -- Level 1 (2 functions): + * ^ f caller src/a.ts:10 + * ^ m Caller.method src/b.ts:20 + * ---- Level 2 (1 functions): + * ^ f transitiveCaller src/c.ts:5 + */ +export function renderImpactLevels( + levels: Record, + opts: RenderImpactLevelsOpts = {}, +): void { + const { emptyMessage = ' No callers found.', limit = 20 } = opts; + + if (Object.keys(levels).length === 0) { + if (emptyMessage) console.log(emptyMessage); + return; + } + + for (const [level, fns] of Object.entries(levels).sort((a, b) => Number(a[0]) - Number(b[0]))) { + const l = parseInt(level, 10); + console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`); + for (const f of fns.slice(0, limit)) { + console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`); + } + if (fns.length > limit) console.log(` ... and ${fns.length - limit} more`); + } +} diff --git a/src/presentation/queries-cli/impact.ts b/src/presentation/queries-cli/impact.ts index 94b1fa64d..16cc0fe7a 100644 --- a/src/presentation/queries-cli/impact.ts +++ b/src/presentation/queries-cli/impact.ts @@ -8,6 +8,7 @@ import { kindIcon, } from '../../domain/queries.js'; import { outputResult } from '../../infrastructure/result-formatter.js'; +import { renderImpactLevels } from '../impact-levels.js'; interface SymbolRef { kind: string; @@ -228,20 +229,6 @@ export function impactAnalysis(file: string, customDbPath: string, opts: OutputO console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`); } -function printFnImpactLevels(levels: Record): void { - if (Object.keys(levels).length === 0) { - console.log(` No callers found.`); - return; - } - for (const [level, fns] of Object.entries(levels).sort((a, b) => Number(a[0]) - Number(b[0]))) { - const l = parseInt(level, 10); - console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`); - for (const f of fns.slice(0, 20)) - console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`); - if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`); - } -} - export function fnImpact(name: string, customDbPath: string, opts: OutputOpts = {}): void { const data = fnImpactData(name, customDbPath, opts) as unknown as FnImpactData; if (outputResult(data as unknown as Record, 'results', opts)) return; @@ -253,7 +240,7 @@ export function fnImpact(name: string, customDbPath: string, opts: OutputOpts = for (const r of data.results) { console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`); - printFnImpactLevels(r.levels); + renderImpactLevels(r.levels); console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`); } } From 7b09bedbc80b23202cc84ce9691d7f753fa31c9a Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Mon, 6 Jul 2026 15:34:13 -0600 Subject: [PATCH 2/2] fix: use strict null check for emptyMessage suppression (Greptile) --- src/presentation/impact-levels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/impact-levels.ts b/src/presentation/impact-levels.ts index ce8262ce7..6ed044f2e 100644 --- a/src/presentation/impact-levels.ts +++ b/src/presentation/impact-levels.ts @@ -45,7 +45,7 @@ export function renderImpactLevels( const { emptyMessage = ' No callers found.', limit = 20 } = opts; if (Object.keys(levels).length === 0) { - if (emptyMessage) console.log(emptyMessage); + if (emptyMessage !== null) console.log(emptyMessage); return; }