@@ -17,10 +17,14 @@ import type {
1717 CodebaseMapPattern ,
1818 CodebaseMapExample ,
1919 CodebaseMapNextCall ,
20+ CodebaseMapKeyInterface ,
21+ CodebaseMapApiSurface ,
22+ CodebaseMapHotspot ,
2023 IntelligenceData ,
21- PatternsData
24+ PatternsData ,
25+ CodeChunk
2226} from '../types/index.js' ;
23- import { RELATIONSHIPS_FILENAME } from '../constants/codebase-context.js' ;
27+ import { RELATIONSHIPS_FILENAME , KEYWORD_INDEX_FILENAME } from '../constants/codebase-context.js' ;
2428
2529// ---------------------------------------------------------------------------
2630// Internal types for relationships.json
@@ -29,6 +33,7 @@ import { RELATIONSHIPS_FILENAME } from '../constants/codebase-context.js';
2933interface RelationshipsGraph {
3034 imports ?: Record < string , string [ ] > ;
3135 importedBy ?: Record < string , string [ ] > ;
36+ exports ?: Record < string , Array < { name : string ; type : string } > > ;
3237 stats ?: {
3338 files ?: number ;
3439 edges ?: number ;
@@ -58,7 +63,7 @@ const ENTRYPOINT_EXCLUSION_RE =
5863
5964/**
6065 * Build a `CodebaseMapSummary` from the project's index artifacts.
61- * Reads `intelligence.json` and `relationships .json` from project paths.
66+ * Reads `intelligence.json`, `relationships.json`, and `index .json` from project paths.
6267 * Degrades gracefully when artifacts are missing.
6368 */
6469export async function buildCodebaseMap ( project : ProjectState ) : Promise < CodebaseMapSummary > {
@@ -83,9 +88,21 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
8388 // Degrade gracefully
8489 }
8590
91+ // Read index.json (keyword index — contains CodeChunk[] with ChunkMetadata)
92+ const idxPath = path . join ( path . dirname ( project . paths . intelligence ) , KEYWORD_INDEX_FILENAME ) ;
93+ let chunks : CodeChunk [ ] = [ ] ;
94+ try {
95+ const raw = await fs . readFile ( idxPath , 'utf-8' ) ;
96+ const parsed = JSON . parse ( raw ) as { chunks ?: unknown } ;
97+ if ( parsed && Array . isArray ( parsed . chunks ) ) chunks = parsed . chunks as CodeChunk [ ] ;
98+ } catch {
99+ // Degrade gracefully
100+ }
101+
86102 const graph = relationships . graph ?? { } ;
87103 const graphImports = graph . imports ?? { } ;
88104 const graphImportedBy = graph . importedBy ?? { } ;
105+ const graphExports = graph . exports ?? { } ;
89106 // relationships.json has stats at top level OR inside graph
90107 const statsSource =
91108 relationships . stats ??
@@ -105,11 +122,12 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
105122 layerCounts . set ( segment , ( layerCounts . get ( segment ) ?? 0 ) + 1 ) ;
106123 }
107124 }
108- const layers : CodebaseMapLayer [ ] = sortByCountThenAlpha (
125+ const rawLayers : CodebaseMapLayer [ ] = sortByCountThenAlpha (
109126 Array . from ( layerCounts . entries ( ) ) . map ( ( [ name , fileCount ] ) => ( { name, fileCount } ) ) ,
110127 ( l ) => l . fileCount ,
111128 ( l ) => l . name
112129 ) ;
130+ const layers = enrichLayers ( rawLayers , graphImportedBy , graphExports ) ;
113131
114132 // --- Entrypoints ---
115133 const entrypoints : string [ ] = [ ] ;
@@ -135,6 +153,15 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
135153 . slice ( 0 , 5 )
136154 . map ( ( x ) => x . file ) ;
137155
156+ // --- Key interfaces ---
157+ const keyInterfaces = deriveKeyInterfaces ( chunks , graphImportedBy ) ;
158+
159+ // --- API surface ---
160+ const apiSurface = deriveApiSurface ( entrypoints , graphExports ) ;
161+
162+ // --- Dependency hotspots ---
163+ const hotspots = deriveHotspots ( graphImports , graphImportedBy ) ;
164+
138165 // --- Active patterns ---
139166 const patterns : PatternsData = intelligence . patterns ?? { } ;
140167 const activePatterns : CodebaseMapPattern [ ] = [ ] ;
@@ -183,14 +210,125 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
183210
184211 return {
185212 project : projectName ,
186- architecture : { layers, entrypoints, hubFiles } ,
213+ architecture : { layers, entrypoints, hubFiles, keyInterfaces , apiSurface , hotspots } ,
187214 activePatterns,
188215 bestExamples,
189216 graphStats,
190217 suggestedNextCalls
191218 } ;
192219}
193220
221+ // ---------------------------------------------------------------------------
222+ // Structural skeleton derivations
223+ // ---------------------------------------------------------------------------
224+
225+ const SYMBOL_KINDS = new Set ( [ 'interface' , 'class' , 'type' , 'enum' ] ) ;
226+
227+ function buildSignatureHint ( content : string ) : string {
228+ const lines = content
229+ . split ( '\n' )
230+ . map ( ( l ) => l . trim ( ) )
231+ . filter ( Boolean ) ;
232+ const hint = lines . slice ( 0 , 3 ) . join ( '\n' ) ;
233+ const truncated = hint . length > 200 ? hint . slice ( 0 , 197 ) + '...' : hint ;
234+ return truncated . replace ( / \s * \{ $ / , '' ) . trim ( ) ;
235+ }
236+
237+ function deriveKeyInterfaces (
238+ chunks : CodeChunk [ ] ,
239+ graphImportedBy : Record < string , string [ ] >
240+ ) : CodebaseMapKeyInterface [ ] {
241+ const symbolChunks = chunks . filter (
242+ ( c ) => c . metadata ?. symbolAware === true && SYMBOL_KINDS . has ( c . metadata . symbolKind ?? '' )
243+ ) ;
244+ const scored = symbolChunks . map ( ( c ) => ( {
245+ chunk : c ,
246+ importerCount : graphImportedBy [ c . relativePath ] ?. length ?? 0
247+ } ) ) ;
248+ scored . sort ( ( a , b ) => {
249+ if ( b . importerCount !== a . importerCount ) return b . importerCount - a . importerCount ;
250+ const lenDiff = a . chunk . content . length - b . chunk . content . length ;
251+ if ( lenDiff !== 0 ) return lenDiff ;
252+ return a . chunk . relativePath . localeCompare ( b . chunk . relativePath ) ;
253+ } ) ;
254+ return scored . slice ( 0 , 10 ) . map ( ( { chunk, importerCount } ) => ( {
255+ name : chunk . metadata . symbolName ?? path . basename ( chunk . relativePath ) ,
256+ kind : chunk . metadata . symbolKind ?? 'unknown' ,
257+ file : chunk . relativePath ,
258+ importerCount,
259+ signatureHint : buildSignatureHint ( chunk . content )
260+ } ) ) ;
261+ }
262+
263+ function deriveApiSurface (
264+ entrypoints : string [ ] ,
265+ graphExports : Record < string , Array < { name : string ; type : string } > >
266+ ) : CodebaseMapApiSurface [ ] {
267+ const results : CodebaseMapApiSurface [ ] = [ ] ;
268+ for ( const ep of entrypoints ) {
269+ const exps = graphExports [ ep ] ;
270+ if ( ! exps || exps . length === 0 ) continue ;
271+ const names = exps
272+ . map ( ( e ) => e . name )
273+ . filter ( ( n ) => n && n !== 'default' )
274+ . slice ( 0 , 5 ) ;
275+ if ( names . length === 0 ) continue ;
276+ results . push ( { file : ep , exports : names } ) ;
277+ }
278+ return results ;
279+ }
280+
281+ function deriveHotspots (
282+ graphImports : Record < string , string [ ] > ,
283+ graphImportedBy : Record < string , string [ ] >
284+ ) : CodebaseMapHotspot [ ] {
285+ const allFiles = new Set ( [ ...Object . keys ( graphImports ) , ...Object . keys ( graphImportedBy ) ] ) ;
286+ const hotspots : CodebaseMapHotspot [ ] = [ ] ;
287+ for ( const file of allFiles ) {
288+ const importerCount = graphImportedBy [ file ] ?. length ?? 0 ;
289+ const importCount = graphImports [ file ] ?. length ?? 0 ;
290+ const combined = importerCount + importCount ;
291+ if ( combined === 0 ) continue ;
292+ hotspots . push ( { file, importerCount, importCount, combined } ) ;
293+ }
294+ hotspots . sort ( ( a , b ) => {
295+ if ( b . combined !== a . combined ) return b . combined - a . combined ;
296+ return a . file . localeCompare ( b . file ) ;
297+ } ) ;
298+ return hotspots . slice ( 0 , 5 ) ;
299+ }
300+
301+ function enrichLayers (
302+ layers : CodebaseMapLayer [ ] ,
303+ graphImportedBy : Record < string , string [ ] > ,
304+ graphExports : Record < string , Array < { name : string ; type : string } > >
305+ ) : CodebaseMapLayer [ ] {
306+ return layers . map ( ( layer ) => {
307+ let bestFile : string | undefined ;
308+ let bestCount = 0 ;
309+ for ( const [ file , importers ] of Object . entries ( graphImportedBy ) ) {
310+ if ( file . split ( '/' ) [ 0 ] !== layer . name ) continue ;
311+ if ( importers . length > bestCount ) {
312+ bestCount = importers . length ;
313+ bestFile = file ;
314+ }
315+ }
316+ if ( ! bestFile ) return layer ;
317+ const exps = graphExports [ bestFile ] ;
318+ const hubExports = exps
319+ ? exps
320+ . map ( ( e ) => e . name )
321+ . filter ( ( n ) => n && n !== 'default' )
322+ . slice ( 0 , 3 )
323+ : [ ] ;
324+ return {
325+ ...layer ,
326+ hubFile : bestFile ,
327+ ...( hubExports . length > 0 ? { hubExports } : { } )
328+ } ;
329+ } ) ;
330+ }
331+
194332// ---------------------------------------------------------------------------
195333// Suggested next calls
196334// ---------------------------------------------------------------------------
@@ -253,16 +391,22 @@ export function renderMapMarkdown(map: CodebaseMapSummary): string {
253391 lines . push ( `# Codebase Map — ${ map . project } ` ) ;
254392 lines . push ( '' ) ;
255393
256- // Architecture
394+ // Architecture layers
257395 lines . push ( '## Architecture Layers' ) ;
258396 lines . push ( '' ) ;
259397 if ( map . architecture . layers . length === 0 ) {
260398 lines . push ( '_No index data available._' ) ;
261399 } else {
262400 for ( const layer of map . architecture . layers ) {
263- lines . push (
264- `- **${ layer . name } ** (${ layer . fileCount } file${ layer . fileCount === 1 ? '' : 's' } )`
265- ) ;
401+ let line = `- **${ layer . name } ** (${ layer . fileCount } file${ layer . fileCount === 1 ? '' : 's' } )` ;
402+ if ( layer . hubFile ) {
403+ const exStr =
404+ layer . hubExports && layer . hubExports . length > 0
405+ ? ` → ${ layer . hubExports . join ( ', ' ) } `
406+ : '' ;
407+ line += ` — hub: \`${ layer . hubFile } \`${ exStr } ` ;
408+ }
409+ lines . push ( line ) ;
266410 }
267411 }
268412 lines . push ( '' ) ;
@@ -291,6 +435,51 @@ export function renderMapMarkdown(map: CodebaseMapSummary): string {
291435 }
292436 lines . push ( '' ) ;
293437
438+ // Key Interfaces
439+ lines . push ( '## Key Interfaces' ) ;
440+ lines . push ( '' ) ;
441+ if ( map . architecture . keyInterfaces . length === 0 ) {
442+ lines . push ( '_None detected._' ) ;
443+ } else {
444+ for ( const ki of map . architecture . keyInterfaces ) {
445+ lines . push (
446+ `- **${ ki . name } ** \`${ ki . kind } \` — \`${ ki . file } \` (imported by ${ ki . importerCount } )`
447+ ) ;
448+ if ( ki . signatureHint ) {
449+ lines . push ( ' ```' ) ;
450+ lines . push ( ` ${ ki . signatureHint . split ( '\n' ) . join ( '\n ' ) } ` ) ;
451+ lines . push ( ' ```' ) ;
452+ }
453+ }
454+ }
455+ lines . push ( '' ) ;
456+
457+ // API Surface
458+ lines . push ( '## API Surface' ) ;
459+ lines . push ( '' ) ;
460+ if ( map . architecture . apiSurface . length === 0 ) {
461+ lines . push ( '_None detected._' ) ;
462+ } else {
463+ for ( const s of map . architecture . apiSurface ) {
464+ lines . push ( `- \`${ s . file } \` — exports: ${ s . exports . join ( ', ' ) } ` ) ;
465+ }
466+ }
467+ lines . push ( '' ) ;
468+
469+ // Dependency Hotspots
470+ lines . push ( '## Dependency Hotspots' ) ;
471+ lines . push ( '' ) ;
472+ if ( map . architecture . hotspots . length === 0 ) {
473+ lines . push ( '_None detected._' ) ;
474+ } else {
475+ for ( const h of map . architecture . hotspots ) {
476+ lines . push (
477+ `- \`${ h . file } \` — imported by ${ h . importerCount } , imports ${ h . importCount } (combined: ${ h . combined } )`
478+ ) ;
479+ }
480+ }
481+ lines . push ( '' ) ;
482+
294483 // Patterns
295484 lines . push ( '## Active Patterns' ) ;
296485 lines . push ( '' ) ;
@@ -376,7 +565,11 @@ export function renderMapPretty(map: CodebaseMapSummary): string {
376565 const layerLines =
377566 map . architecture . layers . length === 0
378567 ? [ '(none)' ]
379- : map . architecture . layers . map ( ( l ) => `${ l . name } ${ l . fileCount } files` ) ;
568+ : map . architecture . layers . map ( ( l ) =>
569+ l . hubFile
570+ ? `${ l . name } ${ l . fileCount } files [${ l . hubFile } ]`
571+ : `${ l . name } ${ l . fileCount } files`
572+ ) ;
380573 sections . push ( box ( 'Architecture Layers' , layerLines ) ) ;
381574
382575 const epLines =
@@ -387,6 +580,26 @@ export function renderMapPretty(map: CodebaseMapSummary): string {
387580 map . architecture . hubFiles . length === 0 ? [ '(none detected)' ] : map . architecture . hubFiles ;
388581 sections . push ( box ( 'Hub Files' , hubLines ) ) ;
389582
583+ const kiLines =
584+ map . architecture . keyInterfaces . length === 0
585+ ? [ '(none detected)' ]
586+ : map . architecture . keyInterfaces . map (
587+ ( ki ) => `${ ki . name } ${ ki . kind } ${ ki . file } (×${ ki . importerCount } )`
588+ ) ;
589+ sections . push ( box ( 'Key Interfaces' , kiLines ) ) ;
590+
591+ const apiLines =
592+ map . architecture . apiSurface . length === 0
593+ ? [ '(none detected)' ]
594+ : map . architecture . apiSurface . map ( ( s ) => `${ s . file } : ${ s . exports . join ( ', ' ) } ` ) ;
595+ sections . push ( box ( 'API Surface' , apiLines ) ) ;
596+
597+ const hotspotLines =
598+ map . architecture . hotspots . length === 0
599+ ? [ '(none detected)' ]
600+ : map . architecture . hotspots . map ( ( h ) => `${ h . file } +${ h . importerCount } /-${ h . importCount } ` ) ;
601+ sections . push ( box ( 'Dependency Hotspots' , hotspotLines ) ) ;
602+
390603 const patternLines =
391604 map . activePatterns . length === 0
392605 ? [ '(no patterns)' ]
0 commit comments