44 */
55
66import * as path from 'node:path' ;
7+ import type { LocalGitExtractor } from '../git/extractor' ;
78import type { RepositoryIndexer } from '../indexer' ;
89import type { SearchResult } from '../vector/types' ;
9- import type { CodebaseMap , ExportInfo , HotPath , MapNode , MapOptions } from './types' ;
10+ import type {
11+ ChangeFrequency ,
12+ CodebaseMap ,
13+ ExportInfo ,
14+ HotPath ,
15+ MapNode ,
16+ MapOptions ,
17+ } from './types' ;
1018
1119export * from './types' ;
1220
@@ -21,8 +29,15 @@ const DEFAULT_OPTIONS: Required<MapOptions> = {
2129 smartDepth : false ,
2230 smartDepthThreshold : 10 ,
2331 tokenBudget : 2000 ,
32+ includeChangeFrequency : false ,
2433} ;
2534
35+ /** Context for map generation including optional git extractor */
36+ export interface MapGenerationContext {
37+ indexer : RepositoryIndexer ;
38+ gitExtractor ?: LocalGitExtractor ;
39+ }
40+
2641/**
2742 * Generate a codebase map from indexed documents
2843 *
@@ -32,13 +47,36 @@ const DEFAULT_OPTIONS: Required<MapOptions> = {
3247 */
3348export async function generateCodebaseMap (
3449 indexer : RepositoryIndexer ,
35- options : MapOptions = { }
50+ options ?: MapOptions
51+ ) : Promise < CodebaseMap > ;
52+
53+ /**
54+ * Generate a codebase map with git history context
55+ *
56+ * @param context - Map generation context with indexer and optional git extractor
57+ * @param options - Map generation options
58+ * @returns Codebase map structure
59+ */
60+ export async function generateCodebaseMap (
61+ context : MapGenerationContext ,
62+ options ?: MapOptions
63+ ) : Promise < CodebaseMap > ;
64+
65+ export async function generateCodebaseMap (
66+ indexerOrContext : RepositoryIndexer | MapGenerationContext ,
67+ options ?: MapOptions
3668) : Promise < CodebaseMap > {
37- const opts = { ...DEFAULT_OPTIONS , ...options } ;
69+ const opts = { ...DEFAULT_OPTIONS , ...( options || { } ) } ;
70+
71+ // Normalize input
72+ const context : MapGenerationContext =
73+ 'indexer' in indexerOrContext
74+ ? indexerOrContext
75+ : { indexer : indexerOrContext as RepositoryIndexer } ;
3876
3977 // Get all indexed documents (use a broad search)
4078 // Note: We search with a generic query to get all documents
41- const allDocs = await indexer . search ( 'function class interface type' , {
79+ const allDocs = await context . indexer . search ( 'function class interface type' , {
4280 limit : 10000 ,
4381 scoreThreshold : 0 ,
4482 } ) ;
@@ -53,6 +91,11 @@ export async function generateCodebaseMap(
5391 // Compute hot paths (most referenced files)
5492 const hotPaths = opts . includeHotPaths ? computeHotPaths ( allDocs , opts . maxHotPaths ) : [ ] ;
5593
94+ // Compute change frequency if requested and git extractor is available
95+ if ( opts . includeChangeFrequency && context . gitExtractor ) {
96+ await computeChangeFrequency ( root , context . gitExtractor ) ;
97+ }
98+
5699 return {
57100 root,
58101 totalComponents,
@@ -274,6 +317,86 @@ function countDirectories(node: MapNode): number {
274317 return count ;
275318}
276319
320+ /**
321+ * Compute change frequency for all nodes in the tree
322+ */
323+ async function computeChangeFrequency ( root : MapNode , extractor : LocalGitExtractor ) : Promise < void > {
324+ // Collect all unique directory paths
325+ const dirPaths = collectDirectoryPaths ( root ) ;
326+
327+ // Get date thresholds
328+ const now = new Date ( ) ;
329+ const thirtyDaysAgo = new Date ( now . getTime ( ) - 30 * 24 * 60 * 60 * 1000 ) ;
330+ const ninetyDaysAgo = new Date ( now . getTime ( ) - 90 * 24 * 60 * 60 * 1000 ) ;
331+
332+ // Compute frequency for each directory
333+ const frequencyMap = new Map < string , ChangeFrequency > ( ) ;
334+
335+ for ( const dirPath of dirPaths ) {
336+ try {
337+ // Get commits for this directory in the last 90 days
338+ const commits = await extractor . getCommits ( {
339+ path : dirPath === 'root' ? '.' : dirPath ,
340+ limit : 100 ,
341+ since : ninetyDaysAgo . toISOString ( ) ,
342+ noMerges : true ,
343+ } ) ;
344+
345+ // Count commits in each time window
346+ let last30Days = 0 ;
347+ const last90Days = commits . length ;
348+ let lastCommit : string | undefined ;
349+
350+ for ( const commit of commits ) {
351+ const commitDate = new Date ( commit . author . date ) ;
352+ if ( commitDate >= thirtyDaysAgo ) {
353+ last30Days ++ ;
354+ }
355+ if ( ! lastCommit || commitDate > new Date ( lastCommit ) ) {
356+ lastCommit = commit . author . date ;
357+ }
358+ }
359+
360+ frequencyMap . set ( dirPath , {
361+ last30Days,
362+ last90Days,
363+ lastCommit,
364+ } ) ;
365+ } catch {
366+ // Directory might not exist in git or other error
367+ // Just skip it
368+ }
369+ }
370+
371+ // Apply frequency data to tree nodes
372+ applyChangeFrequency ( root , frequencyMap ) ;
373+ }
374+
375+ /**
376+ * Collect all directory paths from the tree
377+ */
378+ function collectDirectoryPaths ( node : MapNode , paths : string [ ] = [ ] ) : string [ ] {
379+ paths . push ( node . path ) ;
380+ for ( const child of node . children ) {
381+ collectDirectoryPaths ( child , paths ) ;
382+ }
383+ return paths ;
384+ }
385+
386+ /**
387+ * Apply change frequency data to tree nodes
388+ */
389+ function applyChangeFrequency ( node : MapNode , frequencyMap : Map < string , ChangeFrequency > ) : void {
390+ const freq = frequencyMap . get ( node . path ) ;
391+ if ( freq ) {
392+ node . changeFrequency = freq ;
393+ }
394+
395+ for ( const child of node . children ) {
396+ applyChangeFrequency ( child , frequencyMap ) ;
397+ }
398+ }
399+
277400/**
278401 * Compute hot paths - files with the most incoming references
279402 */
@@ -369,7 +492,23 @@ function formatNode(
369492 const connector = isLast ? '└── ' : '├── ' ;
370493 const countStr = node . componentCount > 0 ? ` (${ node . componentCount } components)` : '' ;
371494
372- lines . push ( `${ prefix } ${ connector } ${ node . name } /${ countStr } ` ) ;
495+ // Add change frequency indicator if available
496+ let freqStr = '' ;
497+ if ( opts . includeChangeFrequency && node . changeFrequency ) {
498+ const freq = node . changeFrequency ;
499+ if ( freq . last30Days > 0 ) {
500+ // Hot: 5+ commits in 30 days
501+ if ( freq . last30Days >= 5 ) {
502+ freqStr = ` 🔥 ${ freq . last30Days } commits this month` ;
503+ } else {
504+ freqStr = ` ✏️ ${ freq . last30Days } commits this month` ;
505+ }
506+ } else if ( freq . last90Days > 0 ) {
507+ freqStr = ` 📝 ${ freq . last90Days } commits (90d)` ;
508+ }
509+ }
510+
511+ lines . push ( `${ prefix } ${ connector } ${ node . name } /${ countStr } ${ freqStr } ` ) ;
373512
374513 // Add exports if present
375514 if ( opts . includeExports && node . exports && node . exports . length > 0 ) {
0 commit comments