@@ -21,10 +21,13 @@ import type {
2121 IndexerState ,
2222 IndexOptions ,
2323 IndexStats ,
24+ LanguageStats ,
25+ PackageStats ,
2426 SupportedLanguage ,
2527 UpdateOptions ,
2628} from './types' ;
2729import { getExtensionForLanguage , prepareDocumentsForEmbedding } from './utils' ;
30+ import { aggregateChangeFrequency , calculateChangeFrequency } from './utils/change-frequency.js' ;
2831
2932const INDEXER_VERSION = '1.0.0' ;
3033const DEFAULT_STATE_PATH = '.dev-agent/indexer-state.json' ;
@@ -451,6 +454,14 @@ export class RepositoryIndexer {
451454 const incrementalUpdatesSince = this . state . incrementalUpdatesSince || 0 ;
452455 const warning = this . getStatsWarning ( incrementalUpdatesSince ) ;
453456
457+ // Enrich stats with change frequency (optional, non-blocking)
458+ const enrichedByLanguage = await this . enrichLanguageStatsWithChangeFrequency (
459+ this . state . stats . byLanguage
460+ ) ;
461+ const enrichedByPackage = await this . enrichPackageStatsWithChangeFrequency (
462+ this . state . stats . byPackage
463+ ) ;
464+
454465 const stats = {
455466 filesScanned : this . state . stats . totalFiles ,
456467 documentsExtracted : this . state . stats . totalDocuments ,
@@ -461,9 +472,9 @@ export class RepositoryIndexer {
461472 startTime : this . state . lastIndexTime ,
462473 endTime : this . state . lastIndexTime ,
463474 repositoryPath : this . state . repositoryPath ,
464- byLanguage : this . state . stats . byLanguage ,
475+ byLanguage : enrichedByLanguage ,
465476 byComponentType : this . state . stats . byComponentType ,
466- byPackage : this . state . stats . byPackage ,
477+ byPackage : enrichedByPackage ,
467478 statsMetadata : {
468479 isIncremental : false , // getStats returns full picture
469480 lastFullIndex,
@@ -483,6 +494,114 @@ export class RepositoryIndexer {
483494 return validation . data ;
484495 }
485496
497+ /**
498+ * Enrich language stats with change frequency data
499+ * Non-blocking: returns original stats if git analysis fails
500+ */
501+ private async enrichLanguageStatsWithChangeFrequency (
502+ byLanguage ?: Partial < Record < SupportedLanguage , LanguageStats > >
503+ ) : Promise < Partial < Record < SupportedLanguage , LanguageStats > > | undefined > {
504+ if ( ! byLanguage ) return byLanguage ;
505+
506+ try {
507+ // Calculate change frequency for repository
508+ const changeFreq = await calculateChangeFrequency ( {
509+ repositoryPath : this . config . repositoryPath ,
510+ maxCommits : 1000 ,
511+ } ) ;
512+
513+ // Enrich each language with aggregate stats
514+ const enriched : Partial < Record < SupportedLanguage , LanguageStats > > = { } ;
515+
516+ for ( const [ lang , langStats ] of Object . entries ( byLanguage ) as Array <
517+ [ SupportedLanguage , LanguageStats ]
518+ > ) {
519+ // Filter change frequency by file extension for this language
520+ const langExtensions = this . getExtensionsForLanguage ( lang ) ;
521+ const langFiles = new Map (
522+ [ ...changeFreq . entries ( ) ] . filter ( ( [ filePath ] ) =>
523+ langExtensions . some ( ( ext ) => filePath . endsWith ( ext ) )
524+ )
525+ ) ;
526+
527+ const aggregate = aggregateChangeFrequency ( langFiles ) ;
528+
529+ enriched [ lang ] = {
530+ ...langStats ,
531+ avgCommitsPerFile : aggregate . avgCommitsPerFile ,
532+ lastModified : aggregate . lastModified ?? undefined ,
533+ } ;
534+ }
535+
536+ return enriched ;
537+ } catch ( error ) {
538+ // Git not available or analysis failed - return original stats without change frequency
539+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
540+ console . warn (
541+ `[indexer] Unable to calculate change frequency for language stats: ${ errorMessage } `
542+ ) ;
543+ return byLanguage ;
544+ }
545+ }
546+
547+ /**
548+ * Enrich package stats with change frequency data
549+ * Non-blocking: returns original stats if git analysis fails
550+ */
551+ private async enrichPackageStatsWithChangeFrequency (
552+ byPackage ?: Record < string , PackageStats >
553+ ) : Promise < Record < string , PackageStats > | undefined > {
554+ if ( ! byPackage ) return byPackage ;
555+
556+ try {
557+ // Calculate change frequency for repository
558+ const changeFreq = await calculateChangeFrequency ( {
559+ repositoryPath : this . config . repositoryPath ,
560+ maxCommits : 1000 ,
561+ } ) ;
562+
563+ // Enrich each package with aggregate stats
564+ const enriched : Record < string , PackageStats > = { } ;
565+
566+ for ( const [ pkgPath , pkgStats ] of Object . entries ( byPackage ) ) {
567+ // Filter change frequency by package path
568+ const pkgFiles = new Map (
569+ [ ...changeFreq . entries ( ) ] . filter ( ( [ filePath ] ) => filePath . startsWith ( pkgPath ) )
570+ ) ;
571+
572+ const aggregate = aggregateChangeFrequency ( pkgFiles ) ;
573+
574+ enriched [ pkgPath ] = {
575+ ...pkgStats ,
576+ totalCommits : aggregate . totalCommits ,
577+ lastModified : aggregate . lastModified ?? undefined ,
578+ } ;
579+ }
580+
581+ return enriched ;
582+ } catch ( error ) {
583+ // Git not available or analysis failed - return original stats without change frequency
584+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
585+ console . warn (
586+ `[indexer] Unable to calculate change frequency for package stats: ${ errorMessage } `
587+ ) ;
588+ return byPackage ;
589+ }
590+ }
591+
592+ /**
593+ * Get file extensions for a language
594+ */
595+ private getExtensionsForLanguage ( language : SupportedLanguage ) : string [ ] {
596+ const extensionMap : Record < SupportedLanguage , string [ ] > = {
597+ typescript : [ '.ts' , '.tsx' ] ,
598+ javascript : [ '.js' , '.jsx' , '.mjs' , '.cjs' ] ,
599+ go : [ '.go' ] ,
600+ markdown : [ '.md' , '.markdown' ] ,
601+ } ;
602+ return extensionMap [ language ] || [ ] ;
603+ }
604+
486605 /**
487606 * Apply stat merging using pure functions
488607 * Wrapper around the pure mergeStats function that updates state
0 commit comments