Skip to content

Commit 45f023b

Browse files
committed
feat(core): add codebase map generation module
Add map module for generating hierarchical codebase views: - MapNode, ExportInfo, MapOptions, CodebaseMap types - generateCodebaseMap() builds tree from indexed documents - formatCodebaseMap() renders tree as readable text - Supports depth limiting, focus directory, export display - Groups components by directory with counts Part of #81
1 parent 4117569 commit 45f023b

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './context';
55
export * from './events';
66
export * from './github';
77
export * from './indexer';
8+
export * from './map';
89
export * from './observability';
910
export * from './scanner';
1011
export * from './storage';

packages/core/src/map/index.ts

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/**
2+
* Codebase Map Generator
3+
* Generates a hierarchical view of the codebase structure
4+
*/
5+
6+
import * as path from 'node:path';
7+
import type { RepositoryIndexer } from '../indexer';
8+
import type { SearchResult } from '../vector/types';
9+
import type { CodebaseMap, ExportInfo, MapNode, MapOptions } from './types';
10+
11+
export * from './types';
12+
13+
/** Default options for map generation */
14+
const DEFAULT_OPTIONS: Required<MapOptions> = {
15+
depth: 2,
16+
focus: '',
17+
includeExports: true,
18+
maxExportsPerDir: 5,
19+
tokenBudget: 2000,
20+
};
21+
22+
/**
23+
* Generate a codebase map from indexed documents
24+
*
25+
* @param indexer - Repository indexer with indexed documents
26+
* @param options - Map generation options
27+
* @returns Codebase map structure
28+
*/
29+
export async function generateCodebaseMap(
30+
indexer: RepositoryIndexer,
31+
options: MapOptions = {}
32+
): Promise<CodebaseMap> {
33+
const opts = { ...DEFAULT_OPTIONS, ...options };
34+
35+
// Get all indexed documents (use a broad search)
36+
// Note: We search with a generic query to get all documents
37+
const allDocs = await indexer.search('function class interface type', {
38+
limit: 10000,
39+
scoreThreshold: 0,
40+
});
41+
42+
// Build directory tree from documents
43+
const root = buildDirectoryTree(allDocs, opts);
44+
45+
// Count totals
46+
const totalComponents = countComponents(root);
47+
const totalDirectories = countDirectories(root);
48+
49+
return {
50+
root,
51+
totalComponents,
52+
totalDirectories,
53+
generatedAt: new Date().toISOString(),
54+
};
55+
}
56+
57+
/**
58+
* Build a directory tree from search results
59+
*/
60+
function buildDirectoryTree(docs: SearchResult[], opts: Required<MapOptions>): MapNode {
61+
// Group documents by directory
62+
const byDir = new Map<string, SearchResult[]>();
63+
64+
for (const doc of docs) {
65+
const filePath = (doc.metadata.path as string) || (doc.metadata.file as string) || '';
66+
if (!filePath) continue;
67+
68+
// Apply focus filter
69+
if (opts.focus && !filePath.startsWith(opts.focus)) {
70+
continue;
71+
}
72+
73+
const dir = path.dirname(filePath);
74+
if (!byDir.has(dir)) {
75+
byDir.set(dir, []);
76+
}
77+
byDir.get(dir)!.push(doc);
78+
}
79+
80+
// Build tree structure
81+
const rootName = opts.focus || '.';
82+
const root: MapNode = {
83+
name: rootName === '.' ? 'root' : path.basename(rootName),
84+
path: rootName,
85+
componentCount: 0,
86+
children: [],
87+
exports: [],
88+
};
89+
90+
// Process each directory
91+
for (const [dir, dirDocs] of byDir) {
92+
insertIntoTree(root, dir, dirDocs, opts);
93+
}
94+
95+
// Prune tree to depth
96+
pruneToDepth(root, opts.depth);
97+
98+
// Sort children alphabetically
99+
sortTree(root);
100+
101+
return root;
102+
}
103+
104+
/**
105+
* Insert documents into the tree at the appropriate location
106+
*/
107+
function insertIntoTree(
108+
root: MapNode,
109+
dirPath: string,
110+
docs: SearchResult[],
111+
opts: Required<MapOptions>
112+
): void {
113+
const parts = dirPath.split(path.sep).filter((p) => p && p !== '.');
114+
115+
let current = root;
116+
117+
for (let i = 0; i < parts.length; i++) {
118+
const part = parts[i];
119+
const currentPath = parts.slice(0, i + 1).join(path.sep);
120+
121+
let child = current.children.find((c) => c.name === part);
122+
if (!child) {
123+
child = {
124+
name: part,
125+
path: currentPath,
126+
componentCount: 0,
127+
children: [],
128+
exports: [],
129+
};
130+
current.children.push(child);
131+
}
132+
current = child;
133+
}
134+
135+
// Add component count and exports to the leaf directory
136+
current.componentCount += docs.length;
137+
138+
if (opts.includeExports) {
139+
const exports = extractExports(docs, opts.maxExportsPerDir);
140+
current.exports = current.exports || [];
141+
current.exports.push(...exports);
142+
// Limit total exports
143+
if (current.exports.length > opts.maxExportsPerDir) {
144+
current.exports = current.exports.slice(0, opts.maxExportsPerDir);
145+
}
146+
}
147+
148+
// Propagate counts up the tree
149+
propagateCounts(root);
150+
}
151+
152+
/**
153+
* Extract export information from documents
154+
*/
155+
function extractExports(docs: SearchResult[], maxExports: number): ExportInfo[] {
156+
const exports: ExportInfo[] = [];
157+
158+
for (const doc of docs) {
159+
if (doc.metadata.exported && doc.metadata.name) {
160+
exports.push({
161+
name: doc.metadata.name as string,
162+
type: (doc.metadata.type as string) || 'unknown',
163+
file: (doc.metadata.path as string) || (doc.metadata.file as string) || '',
164+
});
165+
166+
if (exports.length >= maxExports) break;
167+
}
168+
}
169+
170+
return exports;
171+
}
172+
173+
/**
174+
* Propagate component counts up the tree
175+
*/
176+
function propagateCounts(node: MapNode): number {
177+
let total = node.componentCount;
178+
179+
for (const child of node.children) {
180+
total += propagateCounts(child);
181+
}
182+
183+
node.componentCount = total;
184+
return total;
185+
}
186+
187+
/**
188+
* Prune tree to specified depth
189+
*/
190+
function pruneToDepth(node: MapNode, depth: number, currentDepth = 0): void {
191+
if (currentDepth >= depth) {
192+
// At max depth, collapse children
193+
node.children = [];
194+
return;
195+
}
196+
197+
for (const child of node.children) {
198+
pruneToDepth(child, depth, currentDepth + 1);
199+
}
200+
}
201+
202+
/**
203+
* Sort tree children alphabetically
204+
*/
205+
function sortTree(node: MapNode): void {
206+
node.children.sort((a, b) => a.name.localeCompare(b.name));
207+
for (const child of node.children) {
208+
sortTree(child);
209+
}
210+
}
211+
212+
/**
213+
* Count total components in tree
214+
*/
215+
function countComponents(node: MapNode): number {
216+
return node.componentCount;
217+
}
218+
219+
/**
220+
* Count total directories in tree
221+
*/
222+
function countDirectories(node: MapNode): number {
223+
let count = 1; // Count this node
224+
for (const child of node.children) {
225+
count += countDirectories(child);
226+
}
227+
return count;
228+
}
229+
230+
/**
231+
* Format codebase map as readable text
232+
*/
233+
export function formatCodebaseMap(map: CodebaseMap, options: MapOptions = {}): string {
234+
const opts = { ...DEFAULT_OPTIONS, ...options };
235+
const lines: string[] = [];
236+
237+
lines.push('# Codebase Map');
238+
lines.push('');
239+
240+
// Format tree
241+
formatNode(map.root, lines, '', true, opts);
242+
243+
lines.push('');
244+
lines.push(
245+
`**Total:** ${map.totalComponents} indexed components across ${map.totalDirectories} directories`
246+
);
247+
248+
return lines.join('\n');
249+
}
250+
251+
/**
252+
* Format a single node in the tree
253+
*/
254+
function formatNode(
255+
node: MapNode,
256+
lines: string[],
257+
prefix: string,
258+
isLast: boolean,
259+
opts: Required<MapOptions>
260+
): void {
261+
const connector = isLast ? '└── ' : '├── ';
262+
const countStr = node.componentCount > 0 ? ` (${node.componentCount} components)` : '';
263+
264+
lines.push(`${prefix}${connector}${node.name}/${countStr}`);
265+
266+
// Add exports if present
267+
if (opts.includeExports && node.exports && node.exports.length > 0) {
268+
const exportPrefix = prefix + (isLast ? ' ' : '│ ');
269+
const exportNames = node.exports.map((e) => e.name).join(', ');
270+
lines.push(`${exportPrefix}└── exports: ${exportNames}`);
271+
}
272+
273+
// Format children
274+
const childPrefix = prefix + (isLast ? ' ' : '│ ');
275+
for (let i = 0; i < node.children.length; i++) {
276+
const child = node.children[i];
277+
const isChildLast = i === node.children.length - 1;
278+
formatNode(child, lines, childPrefix, isChildLast, opts);
279+
}
280+
}

packages/core/src/map/types.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Codebase Map Types
3+
* Types for representing codebase structure
4+
*/
5+
6+
/**
7+
* A node in the codebase map tree
8+
*/
9+
export interface MapNode {
10+
/** Directory or file name */
11+
name: string;
12+
/** Full path from repository root */
13+
path: string;
14+
/** Number of indexed components in this node (recursive) */
15+
componentCount: number;
16+
/** Child nodes (subdirectories) */
17+
children: MapNode[];
18+
/** Exported symbols from this directory (if includeExports is true) */
19+
exports?: ExportInfo[];
20+
/** Whether this is a leaf node (file, not directory) */
21+
isFile?: boolean;
22+
}
23+
24+
/**
25+
* Information about an exported symbol
26+
*/
27+
export interface ExportInfo {
28+
/** Symbol name */
29+
name: string;
30+
/** Type of export (function, class, interface, type) */
31+
type: string;
32+
/** File where it's defined */
33+
file: string;
34+
}
35+
36+
/**
37+
* Options for generating a codebase map
38+
*/
39+
export interface MapOptions {
40+
/** Maximum depth to traverse (1-5, default: 2) */
41+
depth?: number;
42+
/** Focus on a specific directory path */
43+
focus?: string;
44+
/** Include exported symbols (default: true) */
45+
includeExports?: boolean;
46+
/** Maximum exports to show per directory (default: 5) */
47+
maxExportsPerDir?: number;
48+
/** Token budget for output (default: 2000) */
49+
tokenBudget?: number;
50+
}
51+
52+
/**
53+
* Result of codebase map generation
54+
*/
55+
export interface CodebaseMap {
56+
/** Root node of the map tree */
57+
root: MapNode;
58+
/** Total number of indexed components */
59+
totalComponents: number;
60+
/** Total number of directories */
61+
totalDirectories: number;
62+
/** Generation timestamp */
63+
generatedAt: string;
64+
}

0 commit comments

Comments
 (0)