66import type { LLMClient } from "../types/LLMClient.js"
77import type { ParsedFile , RawSymbol } from "../types/RawSymbol.js"
88
9+ export type SummaryMode = "llm" | "heuristic" | "auto"
10+
911export interface FileSummaryResult {
1012 relativePath : string
1113 shortSummary : string
@@ -17,20 +19,61 @@ export interface ModuleSummaryResult {
1719 shortSummary : string
1820}
1921
22+ export interface SummaryCacheEntry {
23+ gitHash : string
24+ shortSummary : string
25+ detailedSummary : string
26+ updatedAt : number
27+ }
28+
29+ export type SummaryCache = Record < string , SummaryCacheEntry >
30+
2031export class SummaryGenerator {
21- constructor ( private readonly llm : LLMClient ) { }
32+ private mode : SummaryMode
33+
34+ constructor (
35+ private readonly llm : LLMClient ,
36+ options : { mode ?: SummaryMode } = { }
37+ ) {
38+ this . mode = options . mode ?? "auto"
39+ }
40+
41+ setMode ( mode : SummaryMode ) : void {
42+ this . mode = mode
43+ }
44+
45+ async generateFileSummary ( file : ParsedFile ) : Promise < FileSummaryResult > {
46+ if ( this . mode === "heuristic" ) {
47+ return {
48+ relativePath : file . relativePath ,
49+ shortSummary : this . heuristicShortSummary ( file ) ,
50+ detailedSummary : this . heuristicDetailedSummary ( file ) ,
51+ }
52+ }
53+
54+ try {
55+ return await this . generateFileSummaryLLM ( file )
56+ } catch {
57+ return {
58+ relativePath : file . relativePath ,
59+ shortSummary : this . heuristicShortSummary ( file ) ,
60+ detailedSummary : this . heuristicDetailedSummary ( file ) ,
61+ }
62+ }
63+ }
2264
2365 /**
2466 * Generate summary cho một file từ symbols của nó.
2567 * Feed chỉ signatures + docComments — không feed full source.
2668 */
27- async generateFileSummary ( file : ParsedFile ) : Promise < FileSummaryResult > {
69+ private async generateFileSummaryLLM ( file : ParsedFile ) : Promise < FileSummaryResult > {
2870 const signaturesText = this . buildSignaturesText ( file . symbols )
2971 const exportsList = file . exports . join ( ", " )
3072
31- const prompt = `You are analyzing a TypeScript file to create a concise index entry.
73+ const prompt = `You are analyzing a source file to create a concise index entry.
3274
3375File: ${ file . relativePath }
76+ Language: ${ file . language }
3477Exports: ${ exportsList || "none" }
3578
3679Symbols (signatures only):
@@ -50,8 +93,8 @@ Respond with ONLY a JSON object in this exact format, no markdown:
5093 } )
5194
5295 const parsed = this . parseJsonResponse ( response . content , {
53- short : ` ${ file . relativePath } — TypeScript module` ,
54- detailed : `Contains: ${ exportsList } ` ,
96+ short : this . heuristicShortSummary ( file ) ,
97+ detailed : this . heuristicDetailedSummary ( file ) ,
5598 } )
5699
57100 return {
@@ -61,30 +104,112 @@ Respond with ONLY a JSON object in this exact format, no markdown:
61104 }
62105 }
63106
107+ private heuristicShortSummary ( file : ParsedFile ) : string {
108+ const exported = file . symbols . filter ( ( s ) => s . isExported ) . map ( ( s ) => s . name )
109+ const top = ( exported . length > 0 ? exported : file . symbols . map ( ( s ) => s . name ) ) . slice ( 0 , 5 )
110+ const suffix = top . length > 0 ? ` (${ top . join ( ", " ) } )` : ""
111+ return `${ file . relativePath } — ${ file . language } file with ${ file . symbols . length } symbols${ suffix } `
112+ }
113+
114+ private heuristicDetailedSummary ( file : ParsedFile ) : string {
115+ const kinds = new Map < string , number > ( )
116+ for ( const s of file . symbols ) {
117+ kinds . set ( s . kind , ( kinds . get ( s . kind ) ?? 0 ) + 1 )
118+ }
119+ const kindText = Array . from ( kinds . entries ( ) )
120+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
121+ . slice ( 0 , 6 )
122+ . map ( ( [ k , v ] ) => `${ k } :${ v } ` )
123+ . join ( ", " )
124+
125+ const exportsList = file . exports . slice ( 0 , 12 ) . join ( ", " )
126+ const internalDeps = file . internalImports . length
127+ const externalDeps = file . externalImports . length
128+
129+ const parts = [
130+ `Exports: ${ exportsList || "none" } ` ,
131+ `Deps: internal ${ internalDeps } , external ${ externalDeps } ` ,
132+ kindText ? `Symbols: ${ kindText } ` : "" ,
133+ ] . filter ( Boolean )
134+
135+ return parts . join ( ". " )
136+ }
137+
64138 /**
65139 * Batch generate summaries cho nhiều files.
66140 * Gọi LLM song song với concurrency limit.
67141 */
68142 async generateFileSummaries (
69143 files : ParsedFile [ ] ,
70- concurrency = 5
144+ concurrency = 5 ,
145+ options : {
146+ cache ?: SummaryCache | undefined
147+ getHash ?: ( ( file : ParsedFile ) => string ) | undefined
148+ } = { }
71149 ) : Promise < Map < string , FileSummaryResult > > {
72150 const results = new Map < string , FileSummaryResult > ( )
151+ const cache = options . cache
152+ const getHash = options . getHash
73153
74154 // Process theo batch để tránh rate limit
75155 for ( let i = 0 ; i < files . length ; i += concurrency ) {
76156 const batch = files . slice ( i , i + concurrency )
77157 const batchResults = await Promise . all (
78158 batch . map ( async ( file ) => {
79159 try {
80- return await this . generateFileSummary ( file )
160+ const gitHash = getHash ? getHash ( file ) : ""
161+ const cached = cache ?. [ file . relativePath ]
162+ if ( cached && cached . gitHash === gitHash && cached . shortSummary && cached . detailedSummary ) {
163+ return {
164+ relativePath : file . relativePath ,
165+ shortSummary : cached . shortSummary ,
166+ detailedSummary : cached . detailedSummary ,
167+ }
168+ }
169+
170+ if ( this . mode === "heuristic" ) {
171+ const result = {
172+ relativePath : file . relativePath ,
173+ shortSummary : this . heuristicShortSummary ( file ) ,
174+ detailedSummary : this . heuristicDetailedSummary ( file ) ,
175+ }
176+ if ( cache && gitHash ) {
177+ cache [ file . relativePath ] = {
178+ gitHash,
179+ shortSummary : result . shortSummary ,
180+ detailedSummary : result . detailedSummary ,
181+ updatedAt : Date . now ( ) ,
182+ }
183+ }
184+ return result
185+ }
186+
187+ const llmResult = await this . generateFileSummaryLLM ( file )
188+ if ( cache && gitHash ) {
189+ cache [ file . relativePath ] = {
190+ gitHash,
191+ shortSummary : llmResult . shortSummary ,
192+ detailedSummary : llmResult . detailedSummary ,
193+ updatedAt : Date . now ( ) ,
194+ }
195+ }
196+ return llmResult
81197 } catch {
82- // Fallback nếu LLM call fail
83- return {
198+ const fallback = {
84199 relativePath : file . relativePath ,
85- shortSummary : ` ${ file . relativePath } — TypeScript module` ,
86- detailedSummary : `Exports: ${ file . exports . join ( ", " ) } ` ,
200+ shortSummary : this . heuristicShortSummary ( file ) ,
201+ detailedSummary : this . heuristicDetailedSummary ( file ) ,
87202 }
203+ const gitHash = getHash ? getHash ( file ) : ""
204+ if ( cache && gitHash ) {
205+ cache [ file . relativePath ] = {
206+ gitHash,
207+ shortSummary : fallback . shortSummary ,
208+ detailedSummary : fallback . detailedSummary ,
209+ updatedAt : Date . now ( ) ,
210+ }
211+ }
212+ return fallback
88213 }
89214 } )
90215 )
@@ -107,11 +232,22 @@ Respond with ONLY a JSON object in this exact format, no markdown:
107232 return { dirPath, shortSummary : `Module at ${ dirPath } ` }
108233 }
109234
235+ if ( this . mode === "heuristic" ) {
236+ const sample = fileSummaries
237+ . map ( ( f ) => f . shortSummary )
238+ . filter ( Boolean )
239+ . slice ( 0 , 6 )
240+ . join ( "; " )
241+
242+ const suffix = sample ? ` — ${ sample } ` : ""
243+ return { dirPath, shortSummary : `${ dirPath } — ${ fileSummaries . length } files${ suffix } ` }
244+ }
245+
110246 const fileList = fileSummaries
111247 . map ( ( f ) => `- ${ f . relativePath } : ${ f . shortSummary } ` )
112248 . join ( "\n" )
113249
114- const prompt = `Summarize this TypeScript module (directory) in 1-2 sentences based on its files.
250+ const prompt = `Summarize this code module (directory) in 1-2 sentences based on its files.
115251
116252Module: ${ dirPath }
117253Files:
0 commit comments