Skip to content

Commit 0366fb5

Browse files
committed
feat(map): add hot paths showing most referenced files
Compute and display files with most incoming references: - Count callers from indexed metadata - Count callees pointing to files - Sort by reference count, limit to maxHotPaths - Display in formatted output with ref counts Hot paths help AI assistants identify central/important code.
1 parent 5dc0177 commit 0366fb5

File tree

3 files changed

+238
-1
lines changed

3 files changed

+238
-1
lines changed

packages/core/src/map/__tests__/map.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,158 @@ describe('Codebase Map', () => {
337337
});
338338
});
339339

340+
describe('Hot Paths', () => {
341+
it('should compute hot paths from callers data', async () => {
342+
const resultsWithCallers: SearchResult[] = [
343+
{
344+
id: 'src/core.ts:coreFunction:1',
345+
score: 0.9,
346+
metadata: {
347+
path: 'src/core.ts',
348+
type: 'function',
349+
name: 'coreFunction',
350+
exported: true,
351+
callers: [
352+
{ name: 'caller1', file: 'src/a.ts', startLine: 10 },
353+
{ name: 'caller2', file: 'src/b.ts', startLine: 20 },
354+
{ name: 'caller3', file: 'src/c.ts', startLine: 30 },
355+
],
356+
},
357+
},
358+
{
359+
id: 'src/utils.ts:utilFunction:1',
360+
score: 0.8,
361+
metadata: {
362+
path: 'src/utils.ts',
363+
type: 'function',
364+
name: 'utilFunction',
365+
exported: true,
366+
callers: [{ name: 'caller1', file: 'src/a.ts', startLine: 15 }],
367+
},
368+
},
369+
];
370+
371+
const indexer = createMockIndexer(resultsWithCallers);
372+
const map = await generateCodebaseMap(indexer, { includeHotPaths: true });
373+
374+
expect(map.hotPaths.length).toBeGreaterThan(0);
375+
// coreFunction should be first (more callers)
376+
expect(map.hotPaths[0].file).toBe('src/core.ts');
377+
expect(map.hotPaths[0].incomingRefs).toBe(3);
378+
});
379+
380+
it('should compute hot paths from callees data', async () => {
381+
const resultsWithCallees: SearchResult[] = [
382+
{
383+
id: 'src/main.ts:main:1',
384+
score: 0.9,
385+
metadata: {
386+
path: 'src/main.ts',
387+
type: 'function',
388+
name: 'main',
389+
exported: true,
390+
callees: [
391+
{ name: 'helper', file: 'src/helpers.ts', line: 10 },
392+
{ name: 'helper', file: 'src/helpers.ts', line: 10 },
393+
],
394+
},
395+
},
396+
{
397+
id: 'src/other.ts:other:1',
398+
score: 0.8,
399+
metadata: {
400+
path: 'src/other.ts',
401+
type: 'function',
402+
name: 'other',
403+
exported: true,
404+
callees: [{ name: 'helper', file: 'src/helpers.ts', line: 10 }],
405+
},
406+
},
407+
];
408+
409+
const indexer = createMockIndexer(resultsWithCallees);
410+
const map = await generateCodebaseMap(indexer, { includeHotPaths: true });
411+
412+
expect(map.hotPaths.length).toBeGreaterThan(0);
413+
// helpers.ts should be referenced most
414+
expect(map.hotPaths[0].file).toBe('src/helpers.ts');
415+
expect(map.hotPaths[0].incomingRefs).toBe(3);
416+
});
417+
418+
it('should limit hot paths to maxHotPaths', async () => {
419+
const manyRefs: SearchResult[] = Array.from({ length: 20 }, (_, i) => ({
420+
id: `src/file${i}.ts:fn:1`,
421+
score: 0.9,
422+
metadata: {
423+
path: `src/file${i}.ts`,
424+
type: 'function',
425+
name: `fn${i}`,
426+
exported: true,
427+
callers: Array.from({ length: 20 - i }, (_, j) => ({
428+
name: `caller${j}`,
429+
file: `src/other${j}.ts`,
430+
startLine: j * 10,
431+
})),
432+
},
433+
}));
434+
435+
const indexer = createMockIndexer(manyRefs);
436+
const map = await generateCodebaseMap(indexer, { includeHotPaths: true, maxHotPaths: 3 });
437+
438+
expect(map.hotPaths.length).toBe(3);
439+
// Should be sorted by refs descending
440+
expect(map.hotPaths[0].incomingRefs).toBeGreaterThanOrEqual(map.hotPaths[1].incomingRefs);
441+
});
442+
443+
it('should not include hot paths when disabled', async () => {
444+
const resultsWithCallers: SearchResult[] = [
445+
{
446+
id: 'src/core.ts:coreFunction:1',
447+
score: 0.9,
448+
metadata: {
449+
path: 'src/core.ts',
450+
type: 'function',
451+
name: 'coreFunction',
452+
exported: true,
453+
callers: [{ name: 'caller1', file: 'src/a.ts', startLine: 10 }],
454+
},
455+
},
456+
];
457+
458+
const indexer = createMockIndexer(resultsWithCallers);
459+
const map = await generateCodebaseMap(indexer, { includeHotPaths: false });
460+
461+
expect(map.hotPaths.length).toBe(0);
462+
});
463+
464+
it('should format hot paths in output', async () => {
465+
const resultsWithCallers: SearchResult[] = [
466+
{
467+
id: 'src/core.ts:coreFunction:1',
468+
score: 0.9,
469+
metadata: {
470+
path: 'src/core.ts',
471+
type: 'function',
472+
name: 'coreFunction',
473+
exported: true,
474+
callers: [
475+
{ name: 'caller1', file: 'src/a.ts', startLine: 10 },
476+
{ name: 'caller2', file: 'src/b.ts', startLine: 20 },
477+
],
478+
},
479+
},
480+
];
481+
482+
const indexer = createMockIndexer(resultsWithCallers);
483+
const map = await generateCodebaseMap(indexer, { includeHotPaths: true });
484+
const output = formatCodebaseMap(map, { includeHotPaths: true });
485+
486+
expect(output).toContain('## Hot Paths');
487+
expect(output).toContain('src/core.ts');
488+
expect(output).toContain('2 refs');
489+
});
490+
});
491+
340492
describe('Edge Cases', () => {
341493
it('should handle empty results', async () => {
342494
const indexer = createMockIndexer([]);

packages/core/src/map/index.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as path from 'node:path';
77
import type { RepositoryIndexer } from '../indexer';
88
import type { SearchResult } from '../vector/types';
9-
import type { CodebaseMap, ExportInfo, MapNode, MapOptions } from './types';
9+
import type { CodebaseMap, ExportInfo, HotPath, MapNode, MapOptions } from './types';
1010

1111
export * from './types';
1212

@@ -16,6 +16,8 @@ const DEFAULT_OPTIONS: Required<MapOptions> = {
1616
focus: '',
1717
includeExports: true,
1818
maxExportsPerDir: 5,
19+
includeHotPaths: true,
20+
maxHotPaths: 5,
1921
tokenBudget: 2000,
2022
};
2123

@@ -46,10 +48,14 @@ export async function generateCodebaseMap(
4648
const totalComponents = countComponents(root);
4749
const totalDirectories = countDirectories(root);
4850

51+
// Compute hot paths (most referenced files)
52+
const hotPaths = opts.includeHotPaths ? computeHotPaths(allDocs, opts.maxHotPaths) : [];
53+
4954
return {
5055
root,
5156
totalComponents,
5257
totalDirectories,
58+
hotPaths,
5359
generatedAt: new Date().toISOString(),
5460
};
5561
}
@@ -228,6 +234,54 @@ function countDirectories(node: MapNode): number {
228234
return count;
229235
}
230236

237+
/**
238+
* Compute hot paths - files with the most incoming references
239+
*/
240+
function computeHotPaths(docs: SearchResult[], maxPaths: number): HotPath[] {
241+
// Count incoming references per file
242+
const refCounts = new Map<string, { count: number; component?: string }>();
243+
244+
for (const doc of docs) {
245+
const callers = doc.metadata.callers as Array<{ file: string }> | undefined;
246+
if (callers && Array.isArray(callers)) {
247+
// This document is called by others - count it
248+
const filePath = (doc.metadata.path as string) || (doc.metadata.file as string) || '';
249+
if (filePath) {
250+
const existing = refCounts.get(filePath) || { count: 0 };
251+
existing.count += callers.length;
252+
existing.component = existing.component || (doc.metadata.name as string);
253+
refCounts.set(filePath, existing);
254+
}
255+
}
256+
}
257+
258+
// Also count based on callees pointing to files
259+
for (const doc of docs) {
260+
const callees = doc.metadata.callees as Array<{ file: string; name: string }> | undefined;
261+
if (callees && Array.isArray(callees)) {
262+
for (const callee of callees) {
263+
if (callee.file) {
264+
const existing = refCounts.get(callee.file) || { count: 0 };
265+
existing.count += 1;
266+
refCounts.set(callee.file, existing);
267+
}
268+
}
269+
}
270+
}
271+
272+
// Sort by count and take top N
273+
const sorted = Array.from(refCounts.entries())
274+
.map(([file, data]) => ({
275+
file,
276+
incomingRefs: data.count,
277+
primaryComponent: data.component,
278+
}))
279+
.sort((a, b) => b.incomingRefs - a.incomingRefs)
280+
.slice(0, maxPaths);
281+
282+
return sorted;
283+
}
284+
231285
/**
232286
* Format codebase map as readable text
233287
*/
@@ -238,7 +292,20 @@ export function formatCodebaseMap(map: CodebaseMap, options: MapOptions = {}): s
238292
lines.push('# Codebase Map');
239293
lines.push('');
240294

295+
// Format hot paths if present
296+
if (opts.includeHotPaths && map.hotPaths.length > 0) {
297+
lines.push('## Hot Paths (most referenced)');
298+
for (let i = 0; i < map.hotPaths.length; i++) {
299+
const hp = map.hotPaths[i];
300+
const component = hp.primaryComponent ? ` (${hp.primaryComponent})` : '';
301+
lines.push(`${i + 1}. \`${hp.file}\`${component} - ${hp.incomingRefs} refs`);
302+
}
303+
lines.push('');
304+
}
305+
241306
// Format tree
307+
lines.push('## Directory Structure');
308+
lines.push('');
242309
formatNode(map.root, lines, '', true, opts);
243310

244311
lines.push('');

packages/core/src/map/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,26 @@ export interface MapOptions {
4747
includeExports?: boolean;
4848
/** Maximum exports to show per directory (default: 5) */
4949
maxExportsPerDir?: number;
50+
/** Include hot paths - most referenced files (default: true) */
51+
includeHotPaths?: boolean;
52+
/** Maximum hot paths to show (default: 5) */
53+
maxHotPaths?: number;
5054
/** Token budget for output (default: 2000) */
5155
tokenBudget?: number;
5256
}
5357

58+
/**
59+
* Information about a frequently referenced file
60+
*/
61+
export interface HotPath {
62+
/** File path */
63+
file: string;
64+
/** Number of incoming references (callers) */
65+
incomingRefs: number;
66+
/** Primary component name in this file */
67+
primaryComponent?: string;
68+
}
69+
5470
/**
5571
* Result of codebase map generation
5672
*/
@@ -61,6 +77,8 @@ export interface CodebaseMap {
6177
totalComponents: number;
6278
/** Total number of directories */
6379
totalDirectories: number;
80+
/** Most referenced files (hot paths) */
81+
hotPaths: HotPath[];
6482
/** Generation timestamp */
6583
generatedAt: string;
6684
}

0 commit comments

Comments
 (0)