Skip to content

Commit a4f12aa

Browse files
authored
refactor: extract rendering sub-functions from inspect and diff-impact-mermaid (#767)
1 parent a1f2c52 commit a4f12aa

2 files changed

Lines changed: 204 additions & 143 deletions

File tree

src/presentation/diff-impact-mermaid.ts

Lines changed: 104 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,48 @@
11
import { diffImpactData } from '../domain/analysis/diff-impact.js';
22

3-
export function diffImpactMermaid(
4-
customDbPath: string,
5-
opts: {
6-
noTests?: boolean;
7-
depth?: number;
8-
staged?: boolean;
9-
ref?: string;
10-
includeImplementors?: boolean;
11-
limit?: number;
12-
offset?: number;
13-
config?: any;
14-
} = {},
15-
): string {
16-
const data: any = diffImpactData(customDbPath, opts);
17-
if ('error' in data) return data.error as string;
18-
if (data.changedFiles === 0 || data.affectedFunctions.length === 0) {
19-
return 'flowchart TB\n none["No impacted functions detected"]';
20-
}
3+
interface MermaidNodeRegistry {
4+
nodeIdMap: Map<string, string>;
5+
nodeLabels: Map<string, string>;
6+
counter: number;
7+
}
218

22-
const newFileSet = new Set(data.newFiles || []);
23-
const lines = ['flowchart TB'];
9+
interface ImpactEdgeSets {
10+
allEdges: Set<string>;
11+
edgeFromNodes: Set<string>;
12+
edgeToNodes: Set<string>;
13+
changedKeys: Set<string>;
14+
}
2415

25-
// Assign stable Mermaid node IDs
26-
let nodeCounter = 0;
27-
const nodeIdMap = new Map<string, string>();
28-
const nodeLabels = new Map<string, string>();
29-
function nodeId(key: string, label?: string): string {
30-
if (!nodeIdMap.has(key)) {
31-
nodeIdMap.set(key, `n${nodeCounter++}`);
32-
if (label) nodeLabels.set(key, label);
33-
}
34-
return nodeIdMap.get(key)!;
16+
function createNodeRegistry(): MermaidNodeRegistry {
17+
return { nodeIdMap: new Map(), nodeLabels: new Map(), counter: 0 };
18+
}
19+
20+
function registerNode(reg: MermaidNodeRegistry, key: string, label?: string): string {
21+
if (!reg.nodeIdMap.has(key)) {
22+
reg.nodeIdMap.set(key, `n${reg.counter++}`);
23+
if (label) reg.nodeLabels.set(key, label);
3524
}
25+
return reg.nodeIdMap.get(key)!;
26+
}
3627

37-
// Register all nodes (changed functions + their callers)
38-
for (const fn of data.affectedFunctions) {
39-
nodeId(`${fn.file}::${fn.name}:${fn.line}`, fn.name);
28+
function registerAllNodes(reg: MermaidNodeRegistry, affectedFunctions: any[]): void {
29+
for (const fn of affectedFunctions) {
30+
registerNode(reg, `${fn.file}::${fn.name}:${fn.line}`, fn.name);
4031
for (const callers of Object.values(fn.levels || {})) {
4132
for (const c of callers as Array<{ name: string; file: string; line: number }>) {
42-
nodeId(`${c.file}::${c.name}:${c.line}`, c.name);
33+
registerNode(reg, `${c.file}::${c.name}:${c.line}`, c.name);
4334
}
4435
}
4536
}
37+
}
4638

47-
// Collect all edges and determine blast radius
39+
function collectEdges(affectedFunctions: any[]): ImpactEdgeSets {
4840
const allEdges = new Set<string>();
4941
const edgeFromNodes = new Set<string>();
5042
const edgeToNodes = new Set<string>();
5143
const changedKeys = new Set<string>();
5244

53-
for (const fn of data.affectedFunctions) {
45+
for (const fn of affectedFunctions) {
5446
changedKeys.add(`${fn.file}::${fn.name}:${fn.line}`);
5547
for (const edge of fn.edges || []) {
5648
const edgeKey = `${edge.from}|${edge.to}`;
@@ -62,30 +54,42 @@ export function diffImpactMermaid(
6254
}
6355
}
6456

65-
// Blast radius: caller nodes that are never a source (leaf nodes of the impact tree)
57+
return { allEdges, edgeFromNodes, edgeToNodes, changedKeys };
58+
}
59+
60+
function classifyCallerNodes(edges: ImpactEdgeSets): {
61+
blastRadiusKeys: Set<string>;
62+
intermediateKeys: Set<string>;
63+
} {
6664
const blastRadiusKeys = new Set<string>();
67-
for (const key of edgeToNodes) {
68-
if (!edgeFromNodes.has(key) && !changedKeys.has(key)) {
65+
for (const key of edges.edgeToNodes) {
66+
if (!edges.edgeFromNodes.has(key) && !edges.changedKeys.has(key)) {
6967
blastRadiusKeys.add(key);
7068
}
7169
}
7270

73-
// Intermediate callers: not changed, not blast radius
7471
const intermediateKeys = new Set<string>();
75-
for (const key of edgeToNodes) {
76-
if (!changedKeys.has(key) && !blastRadiusKeys.has(key)) {
72+
for (const key of edges.edgeToNodes) {
73+
if (!edges.changedKeys.has(key) && !blastRadiusKeys.has(key)) {
7774
intermediateKeys.add(key);
7875
}
7976
}
8077

81-
// Group changed functions by file
82-
const fileGroups = new Map<string, typeof data.affectedFunctions>();
83-
for (const fn of data.affectedFunctions) {
78+
return { blastRadiusKeys, intermediateKeys };
79+
}
80+
81+
function emitFileSubgraphs(
82+
lines: string[],
83+
affectedFunctions: any[],
84+
newFileSet: Set<string>,
85+
reg: MermaidNodeRegistry,
86+
): number {
87+
const fileGroups = new Map<string, any[]>();
88+
for (const fn of affectedFunctions) {
8489
if (!fileGroups.has(fn.file)) fileGroups.set(fn.file, []);
8590
fileGroups.get(fn.file)!.push(fn);
8691
}
8792

88-
// Emit changed-file subgraphs
8993
let sgCounter = 0;
9094
for (const [file, fns] of fileGroups) {
9195
const isNew = newFileSet.has(file);
@@ -94,33 +98,71 @@ export function diffImpactMermaid(
9498
lines.push(` subgraph ${sgId}["${file} **(${tag})**"]`);
9599
for (const fn of fns) {
96100
const key = `${fn.file}::${fn.name}:${fn.line}`;
97-
lines.push(` ${nodeIdMap.get(key)}["${fn.name}"]`);
101+
lines.push(` ${reg.nodeIdMap.get(key)}["${fn.name}"]`);
98102
}
99103
lines.push(' end');
100104
const style = isNew ? 'fill:#e8f5e9,stroke:#4caf50' : 'fill:#fff3e0,stroke:#ff9800';
101105
lines.push(` style ${sgId} ${style}`);
102106
}
103107

104-
// Emit intermediate caller nodes (outside subgraphs)
105-
for (const key of intermediateKeys) {
106-
lines.push(` ${nodeIdMap.get(key)}["${nodeLabels.get(key)}"]`);
108+
return sgCounter;
109+
}
110+
111+
function emitBlastRadiusSubgraph(
112+
lines: string[],
113+
blastRadiusKeys: Set<string>,
114+
reg: MermaidNodeRegistry,
115+
sgCounter: number,
116+
): void {
117+
if (blastRadiusKeys.size === 0) return;
118+
const sgId = `sg${sgCounter}`;
119+
lines.push(` subgraph ${sgId}["Callers **(blast radius)**"]`);
120+
for (const key of blastRadiusKeys) {
121+
lines.push(` ${reg.nodeIdMap.get(key)}["${reg.nodeLabels.get(key)}"]`);
107122
}
123+
lines.push(' end');
124+
lines.push(` style ${sgId} fill:#f3e5f5,stroke:#9c27b0`);
125+
}
108126

109-
// Emit blast radius subgraph
110-
if (blastRadiusKeys.size > 0) {
111-
const sgId = `sg${sgCounter++}`;
112-
lines.push(` subgraph ${sgId}["Callers **(blast radius)**"]`);
113-
for (const key of blastRadiusKeys) {
114-
lines.push(` ${nodeIdMap.get(key)}["${nodeLabels.get(key)}"]`);
115-
}
116-
lines.push(' end');
117-
lines.push(` style ${sgId} fill:#f3e5f5,stroke:#9c27b0`);
127+
export function diffImpactMermaid(
128+
customDbPath: string,
129+
opts: {
130+
noTests?: boolean;
131+
depth?: number;
132+
staged?: boolean;
133+
ref?: string;
134+
includeImplementors?: boolean;
135+
limit?: number;
136+
offset?: number;
137+
config?: any;
138+
} = {},
139+
): string {
140+
const data: any = diffImpactData(customDbPath, opts);
141+
if ('error' in data) return data.error as string;
142+
if (data.changedFiles === 0 || data.affectedFunctions.length === 0) {
143+
return 'flowchart TB\n none["No impacted functions detected"]';
118144
}
119145

120-
// Emit edges (impact flows from changed fn toward callers)
121-
for (const edgeKey of allEdges) {
146+
const newFileSet = new Set<string>(data.newFiles || []);
147+
const lines = ['flowchart TB'];
148+
149+
const reg = createNodeRegistry();
150+
registerAllNodes(reg, data.affectedFunctions);
151+
152+
const edges = collectEdges(data.affectedFunctions);
153+
const { blastRadiusKeys, intermediateKeys } = classifyCallerNodes(edges);
154+
155+
const sgCounter = emitFileSubgraphs(lines, data.affectedFunctions, newFileSet, reg);
156+
157+
for (const key of intermediateKeys) {
158+
lines.push(` ${reg.nodeIdMap.get(key)}["${reg.nodeLabels.get(key)}"]`);
159+
}
160+
161+
emitBlastRadiusSubgraph(lines, blastRadiusKeys, reg, sgCounter);
162+
163+
for (const edgeKey of edges.allEdges) {
122164
const [from, to] = edgeKey.split('|') as [string, string];
123-
lines.push(` ${nodeIdMap.get(from)} --> ${nodeIdMap.get(to)}`);
165+
lines.push(` ${reg.nodeIdMap.get(from)} --> ${reg.nodeIdMap.get(to)}`);
124166
}
125167

126168
return lines.join('\n');

0 commit comments

Comments
 (0)