Skip to content

Commit 54917bb

Browse files
committed
feat(map): add smart depth for adaptive tree expansion
Implement information density heuristic for tree pruning: - Expand directories with componentCount >= threshold - Always show first 2 levels for basic structure - Collapse sparse directories to save tokens - Configurable via smartDepth and smartDepthThreshold options Improves token efficiency by focusing on interesting code areas.
1 parent 0366fb5 commit 54917bb

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,125 @@ describe('Codebase Map', () => {
489489
});
490490
});
491491

492+
describe('Smart Depth', () => {
493+
it('should expand dense directories when smartDepth is enabled', async () => {
494+
// Create a structure with varying density
495+
const mixedDensity: SearchResult[] = [
496+
// Dense directory - 15 components
497+
...Array.from({ length: 15 }, (_, i) => ({
498+
id: `packages/core/src/dense/file${i}.ts:fn:1`,
499+
score: 0.9,
500+
metadata: {
501+
path: `packages/core/src/dense/file${i}.ts`,
502+
type: 'function',
503+
name: `fn${i}`,
504+
exported: true,
505+
},
506+
})),
507+
// Sparse directory - 2 components
508+
...Array.from({ length: 2 }, (_, i) => ({
509+
id: `packages/core/src/sparse/file${i}.ts:fn:1`,
510+
score: 0.9,
511+
metadata: {
512+
path: `packages/core/src/sparse/file${i}.ts`,
513+
type: 'function',
514+
name: `fn${i}`,
515+
exported: true,
516+
},
517+
})),
518+
];
519+
520+
const indexer = createMockIndexer(mixedDensity);
521+
const map = await generateCodebaseMap(indexer, {
522+
depth: 5,
523+
smartDepth: true,
524+
smartDepthThreshold: 10,
525+
});
526+
527+
// Find the core node
528+
const findNode = (node: typeof map.root, name: string): typeof map.root | null => {
529+
if (node.name === name) return node;
530+
for (const child of node.children) {
531+
const found = findNode(child, name);
532+
if (found) return found;
533+
}
534+
return null;
535+
};
536+
537+
const srcNode = findNode(map.root, 'src');
538+
expect(srcNode).not.toBeNull();
539+
540+
// Dense should be expanded (has children or is at leaf level)
541+
const denseNode = srcNode?.children.find((c) => c.name === 'dense');
542+
expect(denseNode).toBeDefined();
543+
expect(denseNode?.componentCount).toBe(15);
544+
545+
// Sparse should also exist but may be collapsed
546+
const sparseNode = srcNode?.children.find((c) => c.name === 'sparse');
547+
expect(sparseNode).toBeDefined();
548+
expect(sparseNode?.componentCount).toBe(2);
549+
});
550+
551+
it('should always expand first 2 levels regardless of density', async () => {
552+
const sparseResults: SearchResult[] = [
553+
{
554+
id: 'packages/tiny/src/file.ts:fn:1',
555+
score: 0.9,
556+
metadata: {
557+
path: 'packages/tiny/src/file.ts',
558+
type: 'function',
559+
name: 'fn',
560+
exported: true,
561+
},
562+
},
563+
];
564+
565+
const indexer = createMockIndexer(sparseResults);
566+
const map = await generateCodebaseMap(indexer, {
567+
depth: 5,
568+
smartDepth: true,
569+
smartDepthThreshold: 100, // Very high threshold
570+
});
571+
572+
// Should still show packages and tiny (first 2 levels)
573+
const packagesNode = map.root.children.find((c) => c.name === 'packages');
574+
expect(packagesNode).toBeDefined();
575+
expect(packagesNode?.children.length).toBeGreaterThan(0);
576+
});
577+
578+
it('should not use smart depth when disabled', async () => {
579+
const results: SearchResult[] = Array.from({ length: 5 }, (_, i) => ({
580+
id: `a/b/c/d/e/file${i}.ts:fn:1`,
581+
score: 0.9,
582+
metadata: {
583+
path: `a/b/c/d/e/file${i}.ts`,
584+
type: 'function',
585+
name: `fn${i}`,
586+
exported: true,
587+
},
588+
}));
589+
590+
const indexer = createMockIndexer(results);
591+
const mapWithSmart = await generateCodebaseMap(indexer, {
592+
depth: 3,
593+
smartDepth: true,
594+
smartDepthThreshold: 1,
595+
});
596+
const mapWithoutSmart = await generateCodebaseMap(indexer, {
597+
depth: 3,
598+
smartDepth: false,
599+
});
600+
601+
// Without smart depth, should strictly follow depth limit
602+
const countDepth = (node: typeof mapWithSmart.root, d = 0): number => {
603+
if (node.children.length === 0) return d;
604+
return Math.max(...node.children.map((c) => countDepth(c, d + 1)));
605+
};
606+
607+
expect(countDepth(mapWithoutSmart.root)).toBeLessThanOrEqual(3);
608+
});
609+
});
610+
492611
describe('Edge Cases', () => {
493612
it('should handle empty results', async () => {
494613
const indexer = createMockIndexer([]);

packages/core/src/map/index.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const DEFAULT_OPTIONS: Required<MapOptions> = {
1818
maxExportsPerDir: 5,
1919
includeHotPaths: true,
2020
maxHotPaths: 5,
21+
smartDepth: false,
22+
smartDepthThreshold: 10,
2123
tokenBudget: 2000,
2224
};
2325

@@ -98,8 +100,12 @@ function buildDirectoryTree(docs: SearchResult[], opts: Required<MapOptions>): M
98100
insertIntoTree(root, dir, dirDocs, opts);
99101
}
100102

101-
// Prune tree to depth
102-
pruneToDepth(root, opts.depth);
103+
// Prune tree to depth (smart or fixed)
104+
if (opts.smartDepth) {
105+
smartPruneTree(root, opts.depth, opts.smartDepthThreshold);
106+
} else {
107+
pruneToDepth(root, opts.depth);
108+
}
103109

104110
// Sort children alphabetically
105111
sortTree(root);
@@ -206,6 +212,38 @@ function pruneToDepth(node: MapNode, depth: number, currentDepth = 0): void {
206212
}
207213
}
208214

215+
/**
216+
* Smart prune tree - expand dense directories, collapse sparse ones
217+
* Uses information density heuristic: expand if componentCount >= threshold
218+
*/
219+
function smartPruneTree(
220+
node: MapNode,
221+
maxDepth: number,
222+
threshold: number,
223+
currentDepth = 0
224+
): void {
225+
// Always stop at max depth
226+
if (currentDepth >= maxDepth) {
227+
node.children = [];
228+
return;
229+
}
230+
231+
// For each child, decide whether to expand or collapse
232+
for (const child of node.children) {
233+
// Expand if:
234+
// 1. We're within first 2 levels (always show some structure)
235+
// 2. OR the child has enough components to be "interesting"
236+
const shouldExpand = currentDepth < 2 || child.componentCount >= threshold;
237+
238+
if (shouldExpand) {
239+
smartPruneTree(child, maxDepth, threshold, currentDepth + 1);
240+
} else {
241+
// Collapse this branch - it's too sparse to be interesting
242+
child.children = [];
243+
}
244+
}
245+
}
246+
209247
/**
210248
* Sort tree children alphabetically
211249
*/

packages/core/src/map/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export interface MapOptions {
5151
includeHotPaths?: boolean;
5252
/** Maximum hot paths to show (default: 5) */
5353
maxHotPaths?: number;
54+
/** Use smart depth - expand dense directories, collapse sparse ones (default: false) */
55+
smartDepth?: boolean;
56+
/** Minimum components to expand a directory when using smart depth (default: 10) */
57+
smartDepthThreshold?: number;
5458
/** Token budget for output (default: 2000) */
5559
tokenBudget?: number;
5660
}

0 commit comments

Comments
 (0)