@@ -34,12 +34,23 @@ export async function buildContext(root: string, options: ContextOptions) {
3434 const files = ( await walkSourceFiles ( root , options . path ?? '.' , { maxFiles : 1000 } ) ) . filter ( ( file ) => ! options . changedOnly || changedFiles . has ( file . relativePath ) ) ;
3535 const ranked = [ ] as Array < { path : string ; score : number ; reason : string ; tokens : number ; content : string ; symbolId ?: string } > ;
3636 const fileRecords = [ ] as Array < { path : string ; source : string ; imports : string [ ] ; exported : Array < { name : string ; kind : string } > ; symbolText : string ; tokens : number ; size : number ; content : string ; symbolId ?: string } > ;
37+ const omittedFiles = [ ] as Array < { path : string ; reason : string } > ;
3738 for ( const file of files ) {
38- const { text : source } = await readTextFileSafe ( file . absolutePath , undefined , root ) ;
39- if ( options . includeTests === false && / ( ^ | \/ ) ( t e s t | t e s t s | s p e c | _ _ t e s t s _ _ ) ( \/ | $ ) | \. ( t e s t | s p e c ) \. / . test ( file . relativePath ) ) continue ;
40- const skeleton = await skeletonSourceAsync ( root , file . relativePath , source , { budget : Math . min ( 2000 , budget ) } ) ;
41- const content = JSON . stringify ( skeleton , null , 2 ) ;
42- fileRecords . push ( { path : file . relativePath , source, imports : skeleton . symbols . filter ( ( symbol ) => symbol . kind === 'import' ) . map ( ( symbol ) => symbol . source ?? symbol . signature ) , exported : skeleton . symbols . filter ( ( symbol ) => symbol . exported ) . map ( ( symbol ) => ( { name : symbol . qualifiedName , kind : symbol . kind } ) ) , symbolText : skeleton . symbols . map ( ( symbol ) => `${ symbol . name } ${ symbol . signature } ` ) . join ( '\n' ) , tokens : skeleton . tokenEstimate , size : file . size , content, symbolId : skeleton . symbols . find ( ( symbol ) => symbol . kind !== 'import' ) ?. symbolId } ) ;
39+ if ( options . includeTests === false && / ( ^ | \/ ) ( t e s t | t e s t s | s p e c | _ _ t e s t s _ _ ) ( \/ | $ ) | \. ( t e s t | s p e c ) \. / . test ( file . relativePath ) ) {
40+ omittedFiles . push ( { path : file . relativePath , reason : 'includeTests:false' } ) ;
41+ continue ;
42+ }
43+ try {
44+ const { text : source } = await readTextFileSafe ( file . absolutePath , undefined , root ) ;
45+ const skeleton = await skeletonSourceAsync ( root , file . relativePath , source , { budget : Math . min ( 2000 , budget ) } ) ;
46+ const content = JSON . stringify ( skeleton , null , 2 ) ;
47+ fileRecords . push ( { path : file . relativePath , source, imports : skeleton . symbols . filter ( ( symbol ) => symbol . kind === 'import' ) . map ( ( symbol ) => symbol . source ?? symbol . signature ) , exported : skeleton . symbols . filter ( ( symbol ) => symbol . exported ) . map ( ( symbol ) => ( { name : symbol . qualifiedName , kind : symbol . kind } ) ) , symbolText : skeleton . symbols . map ( ( symbol ) => `${ symbol . name } ${ symbol . signature } ` ) . join ( '\n' ) , tokens : skeleton . tokenEstimate , size : file . size , content, symbolId : skeleton . symbols . find ( ( symbol ) => symbol . kind !== 'import' ) ?. symbolId } ) ;
48+ } catch ( error ) {
49+ const message = error instanceof Error ? error . message : String ( error ) ;
50+ const reason = / F i l e t o o l a r g e / i. test ( message ) ? 'file_too_large' : / B i n a r y / i. test ( message ) ? 'unsupported_binary' : `parse_or_read_error:${ message } ` ;
51+ omittedFiles . push ( { path : file . relativePath , reason } ) ;
52+ warnings . push ( `omitted:${ file . relativePath } :${ reason } ` ) ;
53+ }
4354 }
4455 const fileSet = new Set ( fileRecords . map ( ( record ) => record . path ) ) ;
4556 const graph = summarizeGraph (
@@ -100,15 +111,20 @@ export async function buildContext(root: string, options: ContextOptions) {
100111 }
101112 for ( const item of ranked . filter ( ( rankedItem ) => rankedItem . symbolId ) ) {
102113 if ( usedTokens >= budget ) break ;
103- const body = await readCode ( root , item . path , { symbolId : item . symbolId , maxBytes : Math . min ( 12000 , ( budget - usedTokens ) * 4 ) } ) ;
104- if ( usedTokens + body . tokenEstimate > budget ) continue ;
105- items . push ( { type : 'symbol_body' , path : item . path , symbolId : item . symbolId , score : Number ( item . score . toFixed ( 2 ) ) , reason : 'top ranked symbol body within remaining budget' , content : body . content } ) ;
106- usedTokens += body . tokenEstimate ;
114+ try {
115+ const body = await readCode ( root , item . path , { symbolId : item . symbolId , maxBytes : Math . min ( 12000 , ( budget - usedTokens ) * 4 ) } ) ;
116+ if ( usedTokens + body . tokenEstimate > budget ) continue ;
117+ items . push ( { type : 'symbol_body' , path : item . path , symbolId : item . symbolId , score : Number ( item . score . toFixed ( 2 ) ) , reason : 'top ranked symbol body within remaining budget' , content : body . content } ) ;
118+ usedTokens += body . tokenEstimate ;
119+ } catch ( error ) {
120+ warnings . push ( `body_read_unavailable:${ item . path } :${ error instanceof Error ? error . message : String ( error ) } ` ) ;
121+ }
107122 }
108123 const nextReads = ranked . slice ( 0 , 5 ) . map ( ( item ) => ( { command : item . symbolId ? 'read' : 'skeleton' , path : item . path , symbolId : item . symbolId } ) ) ;
109124 const included = new Set ( items . map ( ( item ) => `${ item . type } :${ item . path } :${ item . symbolId ?? '' } ` ) ) ;
110- const omitted = ranked . filter ( ( item ) => ! included . has ( `skeleton:${ item . path } :` ) ) . slice ( 0 , 20 ) . map ( ( item ) => ( { path : item . path , reason : 'budget' } ) ) ;
111- const data = { schemaVersion : SCHEMA_VERSION , goal : options . goal , budget, usedTokens, items, omitted, nextReads, warnings, truncated : omitted . length > 0 , tokenEstimate : estimateTokens ( JSON . stringify ( items ) ) } ;
125+ const omitted = [ ...ranked . filter ( ( item ) => ! included . has ( `skeleton:${ item . path } :` ) ) . slice ( 0 , 20 ) . map ( ( item ) => ( { path : item . path , reason : 'budget' } ) ) , ...omittedFiles . slice ( 0 , 20 ) ] ;
126+ const testRelations = inferTestRelations ( fileRecords , graph . edges ) . slice ( 0 , 20 ) ;
127+ const data = { schemaVersion : SCHEMA_VERSION , goal : options . goal , budget, usedTokens, items, omitted, nextReads, testRelations, warnings, truncated : omitted . length > 0 , tokenEstimate : estimateTokens ( JSON . stringify ( items ) ) } ;
112128 return data ;
113129}
114130
@@ -136,6 +152,33 @@ function isGeneratedOrVendor(filePath: string): boolean {
136152 return / ( ^ | \/ ) ( v e n d o r | v e n d o r s | t h i r d _ p a r t y | n o d e _ m o d u l e s | d i s t | b u i l d | c o v e r a g e ) ( \/ | $ ) | ( ^ | \/ ) [ ^ / ] + \. ( m i n | g e n e r a t e d | g e n ) \. [ ^ . ] + $ | ( ^ | \/ ) [ ^ / ] + _ ( p b | g e n e r a t e d ) \. [ ^ . ] + $ / . test ( filePath ) ;
137153}
138154
155+ function inferTestRelations ( fileRecords : Array < { path : string ; imports : string [ ] } > , edges : Array < { from : string ; resolved ?: string } > ) {
156+ const sourceFiles = new Set ( fileRecords . map ( ( record ) => record . path ) . filter ( ( filePath ) => ! isTestPath ( filePath ) ) ) ;
157+ const relations : Array < { test : string ; source : string ; reason : string } > = [ ] ;
158+ for ( const test of fileRecords . filter ( ( record ) => isTestPath ( record . path ) ) ) {
159+ for ( const edge of edges . filter ( ( item ) => item . from === test . path && item . resolved && sourceFiles . has ( item . resolved ) ) ) {
160+ relations . push ( { test : test . path , source : edge . resolved ! , reason : 'import' } ) ;
161+ }
162+ const testBase = path . posix . basename ( test . path ) . replace ( / ^ ( t e s t _ | s p e c _ ) / , '' ) . replace ( / ( _ t e s t | \. t e s t | \. s p e c ) ? \. [ ^ . ] + $ / , '' ) . toLowerCase ( ) ;
163+ for ( const source of sourceFiles ) {
164+ const sourceBase = path . posix . basename ( source ) . replace ( / \. [ ^ . ] + $ / , '' ) . toLowerCase ( ) ;
165+ if ( testBase && sourceBase && ( testBase === sourceBase || testBase . includes ( sourceBase ) || sourceBase . includes ( testBase ) ) ) relations . push ( { test : test . path , source, reason : 'name_proximity' } ) ;
166+ }
167+ }
168+ const seen = new Set < string > ( ) ;
169+ return relations . filter ( ( relation ) => {
170+ const key = `${ relation . test } :${ relation . source } ` ;
171+ if ( seen . has ( key ) ) return false ;
172+ seen . add ( key ) ;
173+ return true ;
174+ } ) ;
175+ }
176+
177+ function isTestPath ( filePath : string ) : boolean {
178+ return / ( ^ | \/ ) ( t e s t | t e s t s | s p e c | _ _ t e s t s _ _ ) ( \/ | $ ) | \. ( t e s t | s p e c ) \. | ( ^ | \/ ) t e s t _ [ ^ / ] + \. p y $ | ( ^ | \/ ) [ ^ / ] + _ t e s t \. p y $ / . test ( filePath ) ;
179+ }
180+
139181export function renderContext ( data : Awaited < ReturnType < typeof buildContext > > ) : string {
140- return `Context pack: ${ data . usedTokens } tokens, ${ data . items . length } included\n\n${ data . items . map ( ( item , index ) => `${ index + 1 } . ${ item . path } ${ item . type } (${ item . reason } )` ) . join ( '\n' ) } \n\nNext reads:\n${ data . nextReads . map ( ( item ) => ` codebone ${ item . command } ${ item . path } ` ) . join ( '\n' ) } ` ;
182+ const relatedTests = data . testRelations . length ? `\n\nRelated tests:\n${ data . testRelations . slice ( 0 , 10 ) . map ( ( item ) => ` ${ item . test } -> ${ item . source } (${ item . reason } )` ) . join ( '\n' ) } ` : '' ;
183+ return `Context pack: ${ data . usedTokens } tokens, ${ data . items . length } included\n\n${ data . items . map ( ( item , index ) => `${ index + 1 } . ${ item . path } ${ item . type } (${ item . reason } )` ) . join ( '\n' ) } \n\nNext reads:\n${ data . nextReads . map ( ( item ) => ` codebone ${ item . command } ${ item . path } ` ) . join ( '\n' ) } ${ relatedTests } ` ;
141184}
0 commit comments