@@ -18,7 +18,7 @@ export interface ContextOptions {
1818 budget ?: number ;
1919 includeTests ?: boolean ;
2020 changedOnly ?: boolean ;
21- mode ?: 'full' | 'architecture' | 'overview' | 'edit_prep' | 'composition' ;
21+ mode ?: 'full' | 'architecture' | 'overview' | 'edit_prep' | 'composition' | 'test_impact' ;
2222 productionOnly ?: boolean ;
2323 testsOnly ?: boolean ;
2424 includeMocks ?: boolean ;
@@ -108,14 +108,16 @@ export async function buildContext(root: string, options: ContextOptions) {
108108 const items = [ ] as Array < { type : 'skeleton' | 'symbol_body' ; path : string ; score : number ; reason : string ; content : string ; symbolId ?: string } > ;
109109 let usedTokens = 0 ;
110110 const testRelations = inferTestRelations ( fileRecords , graph . edges ) . slice ( 0 , 30 ) ;
111- if ( options . mode === 'architecture' || options . mode === 'overview' || options . mode === 'edit_prep' || options . mode === 'composition' ) {
111+ if ( options . mode === 'architecture' || options . mode === 'overview' || options . mode === 'edit_prep' || options . mode === 'composition' || options . mode === 'test_impact' ) {
112112 const architecture = buildArchitectureSummary ( fileRecords , graph . edges , testRelations , changedFiles ) ;
113113 const content = options . mode === 'overview'
114114 ? renderOverviewSummary ( architecture )
115115 : options . mode === 'edit_prep'
116116 ? renderEditPrepSummary ( architecture , options . goal )
117117 : options . mode === 'composition'
118118 ? renderCompositionSummary ( architecture , budget )
119+ : options . mode === 'test_impact'
120+ ? renderTestImpactSummary ( architecture , options . goal )
119121 : renderArchitectureSummary ( architecture , budget ) ;
120122 const omitted = omittedFiles . slice ( 0 , 20 ) ;
121123 const data = { schemaVersion : SCHEMA_VERSION , goal : options . goal , mode : options . mode , budget, usedTokens : estimateTokens ( content ) , items : [ { type : `${ options . mode } _summary` as const , path : options . path ?? '.' , score : 1 , reason : `compact ${ options . mode } summary` , content } ] , omitted, nextReads : ranked . slice ( 0 , 10 ) . map ( ( item ) => ( { command : 'skeleton' , path : item . path , symbolId : item . symbolId } ) ) , architecture, testRelations, warnings, truncated : omitted . length > 0 || estimateTokens ( renderArchitectureSummary ( architecture ) ) > budget , tokenEstimate : estimateTokens ( content ) } ;
@@ -247,7 +249,10 @@ function renderArchitectureSummary(summary: ReturnType<typeof buildArchitectureS
247249 if ( summary . dependencies . length ) sections . push ( `App dependency graph:\n${ summary . dependencies . map ( ( item ) => {
248250 const writes = item . writes . length ? ` created: ${ item . writes . map ( ( usage ) => `${ usage . file } :${ usage . line } ${ usage . value ? ` = ${ usage . value } ` : '' } ` ) . join ( ', ' ) } ` : '' ;
249251 const reads = item . reads . length ? ` read: ${ item . reads . slice ( 0 , 6 ) . map ( ( usage ) => `${ usage . file } :${ usage . line } ` ) . join ( ', ' ) } ` : '' ;
250- return ` app["${ item . key } "]${ writes } ${ reads } ` ;
252+ const starts = item . starts . length ? ` start: ${ item . starts . map ( ( usage ) => `${ usage . file } :${ usage . line } ` ) . join ( ', ' ) } ` : '' ;
253+ const stops = item . stops . length ? ` stop: ${ item . stops . map ( ( usage ) => `${ usage . file } :${ usage . line } ` ) . join ( ', ' ) } ` : '' ;
254+ const risk = item . starts . length && ! item . stops . length ? ' [risk: started but no stop found]' : '' ;
255+ return ` app["${ item . key } "]${ writes } ${ reads } ${ starts } ${ stops } ${ risk } ` ;
251256 } ) . join ( '\n' ) } `) ;
252257 if ( summary . tables . length ) sections . push ( `SQLAlchemy tables:\n${ summary . tables . map ( ( item ) => ` ${ item . name } (${ item . file } :${ item . line } )` ) . join ( '\n' ) } ` ) ;
253258 if ( summary . changed . length ) sections . push ( `Changed files impact:\n${ summary . changed . map ( ( item ) => ` ${ item . path } ${ item . tests . length ? ` -> tests: ${ item . tests . join ( ', ' ) } ` : '' } ` ) . join ( '\n' ) } ` ) ;
@@ -297,13 +302,31 @@ function renderCompositionSummary(summary: ReturnType<typeof buildArchitectureSu
297302 if ( summary . dependencies . length ) sections . push ( `App dependencies:\n${ summary . dependencies . map ( ( item ) => {
298303 const writes = item . writes . length ? `created ${ item . writes . map ( ( usage ) => `${ usage . file } :${ usage . line } ${ usage . value ? ` = ${ usage . value } ` : '' } ` ) . join ( ', ' ) } ` : 'no creator found' ;
299304 const reads = item . reads . length ? `; read ${ item . reads . slice ( 0 , 5 ) . map ( ( usage ) => `${ usage . file } :${ usage . line } ` ) . join ( ', ' ) } ` : '' ;
300- return ` app["${ item . key } "] ${ writes } ${ reads } ` ;
305+ const lifecycle = `${ item . starts . length ? `; start ${ item . starts . map ( ( usage ) => `${ usage . file } :${ usage . line } ` ) . join ( ', ' ) } ` : '' } ${ item . stops . length ? `; stop ${ item . stops . map ( ( usage ) => `${ usage . file } :${ usage . line } ` ) . join ( ', ' ) } ` : '' } ${ item . starts . length && ! item . stops . length ? '; risk: started but no stop found' : '' } ` ;
306+ return ` app["${ item . key } "] ${ writes } ${ reads } ${ lifecycle } ` ;
301307 } ) . join ( '\n' ) } `) ;
302308 if ( summary . layers . integrations . length ) sections . push ( `External integrations:\n${ summary . layers . integrations . slice ( 0 , 15 ) . map ( ( file ) => ` ${ file } ` ) . join ( '\n' ) } ` ) ;
303309 if ( summary . layers . background . length ) sections . push ( `Background jobs/consumers:\n${ summary . layers . background . slice ( 0 , 15 ) . map ( ( file ) => ` ${ file } ` ) . join ( '\n' ) } ` ) ;
304310 return fitSections ( sections , budget ) ;
305311}
306312
313+ function renderTestImpactSummary ( summary : ReturnType < typeof buildArchitectureSummary > , goal : string ) : string {
314+ const query = goal . toLowerCase ( ) . split ( / [ ^ a - z 0 - 9 _ . / ] + / ) . filter ( ( term ) => term . length > 2 ) ;
315+ const scored = summary . testRelations . map ( ( relation ) => {
316+ const haystack = `${ relation . source } ${ relation . test } ` . toLowerCase ( ) ;
317+ const score = query . reduce ( ( sum , term ) => sum + ( haystack . includes ( term ) ? 1 : 0 ) , 0 ) + ( relation . reason === 'import' ? 2 : relation . reason === 'symbol_mention' ? 1.5 : 1 ) ;
318+ return { ...relation , score } ;
319+ } ) . filter ( ( relation ) => relation . score > 0 ) . sort ( ( a , b ) => b . score - a . score || a . test . localeCompare ( b . test ) ) . slice ( 0 , 12 ) ;
320+ const sections = [ `Test impact for: ${ goal } ` ] ;
321+ if ( scored . length ) sections . push ( `Pytest candidates:\n${ scored . map ( ( item ) => ` ${ item . test } (covers ${ item . source } ; ${ item . reason } )` ) . join ( '\n' ) } ` ) ;
322+ const fixtureHints = Array . from ( new Set ( scored . flatMap ( ( item ) => [ item . test , item . source ] ) . filter ( ( file ) => / f i x t u r e | m o c k | f a k e | c o n f t e s t / i. test ( file ) ) ) ) ;
323+ const external = Array . from ( new Set ( scored . flatMap ( ( item ) => [ item . test , item . source ] ) . join ( ' ' ) . match ( / r e d i s | r a b b i t | p o s t g r e s | m o n g o | k a f k a / gi) ?? [ ] ) ) . map ( ( item ) => item . toLowerCase ( ) ) ;
324+ if ( fixtureHints . length ) sections . push ( `Fixture/mock hints:\n${ fixtureHints . map ( ( item ) => ` ${ item } ` ) . join ( '\n' ) } ` ) ;
325+ if ( external . length ) sections . push ( `External services hinted by paths: ${ external . join ( ', ' ) } ` ) ;
326+ if ( ! scored . length ) sections . push ( 'No direct test relation found. Use codebone_symbols for the changed symbol, then run nearest API/task tests by path proximity.' ) ;
327+ return sections . join ( '\n\n' ) ;
328+ }
329+
307330function summarizeLayers ( fileRecords : Array < { path : string } > ) {
308331 const layers : Record < string , string [ ] > = { entrypoints : [ ] , api : [ ] , services : [ ] , persistence : [ ] , background : [ ] , integrations : [ ] , tests : [ ] } ;
309332 for ( const record of fileRecords ) {
@@ -330,12 +353,14 @@ function fitSections(sections: string[], budget: number): string {
330353}
331354
332355function buildAppDependencyGraph ( fileRecords : Array < { path : string ; symbols : ReturnType < typeof flattenSymbols > } > ) {
333- const byKey = new Map < string , { key : string ; writes : Array < { file : string ; line : number ; value ?: string } > ; reads : Array < { file : string ; line : number } > } > ( ) ;
356+ const byKey = new Map < string , { key : string ; writes : Array < { file : string ; line : number ; value ?: string } > ; reads : Array < { file : string ; line : number } > ; starts : Array < { file : string ; line : number } > ; stops : Array < { file : string ; line : number } > } > ( ) ;
334357 for ( const record of fileRecords ) {
335358 for ( const symbol of record . symbols . filter ( ( item ) => item . kind === 'dependency' ) ) {
336- const entry = byKey . get ( symbol . name ) ?? { key : symbol . name , writes : [ ] , reads : [ ] } ;
359+ const entry = byKey . get ( symbol . name ) ?? { key : symbol . name , writes : [ ] , reads : [ ] , starts : [ ] , stops : [ ] } ;
337360 const usage = { file : record . path , line : symbol . startLine } ;
338361 if ( / \b (?: a p p | r e q u e s t \. a p p ) \[ [ ' " ] [ ^ ' " ] + [ ' " ] \] \s * = / . test ( symbol . signature ) ) entry . writes . push ( { ...usage , value : symbol . signature . split ( '=' ) . slice ( 1 ) . join ( '=' ) . trim ( ) . slice ( 0 , 80 ) } ) ;
362+ else if ( / \[ [ ' " ] [ ^ ' " ] + [ ' " ] \] \. (?: s t a r t | s t a r t u p ) \s * \( / . test ( symbol . signature ) ) entry . starts . push ( usage ) ;
363+ else if ( / \[ [ ' " ] [ ^ ' " ] + [ ' " ] \] \. (?: s t o p | c l o s e | s h u t d o w n | c l e a n u p ) \s * \( / . test ( symbol . signature ) ) entry . stops . push ( usage ) ;
339364 else entry . reads . push ( usage ) ;
340365 byKey . set ( symbol . name , entry ) ;
341366 }
0 commit comments