@@ -35,7 +35,6 @@ interface Connection {
3535
3636export class TraversalHandler {
3737 private static readonly NODE_NOT_FOUND_QUERY = 'MATCH (n) WHERE n.id = $nodeId RETURN n' ;
38- private static readonly MAX_NODES_PER_CHAIN = 8 ;
3938
4039 constructor ( private neo4jService : Neo4jService ) { }
4140
@@ -47,7 +46,7 @@ export class TraversalHandler {
4746 relationshipTypes,
4847 includeStartNodeDetails = true ,
4948 includeCode = false ,
50- maxNodesPerChain = TraversalHandler . MAX_NODES_PER_CHAIN ,
49+ maxNodesPerChain = 5 ,
5150 summaryOnly = false ,
5251 title = `Node Traversal from: ${ nodeId } ` ,
5352 snippetLength = DEFAULTS . codeSnippetLength ,
@@ -84,7 +83,7 @@ export class TraversalHandler {
8483 } ) ;
8584
8685 return {
87- content : [ { type : 'text' , text : JSON . stringify ( result , null , 2 ) } ] ,
86+ content : [ { type : 'text' , text : JSON . stringify ( result ) } ] ,
8887 } ;
8988 } catch ( error ) {
9089 console . error ( 'Node traversal error:' , error ) ;
@@ -137,25 +136,6 @@ export class TraversalHandler {
137136 ) ;
138137 }
139138
140- private groupConnectionsByRelationshipChain ( connections : Connection [ ] ) : Record < string , Connection [ ] > {
141- return connections . reduce (
142- ( acc , conn ) => {
143- // Build chain with direction arrows
144- const chain =
145- conn . relationshipChain
146- ?. map ( ( rel : any ) => {
147- // rel has: { type, start, end, properties }
148- return rel . type ;
149- } )
150- . join ( ' → ' ) ?? 'Unknown' ;
151- acc [ chain ] ??= [ ] ;
152- acc [ chain ] . push ( conn ) ;
153- return acc ;
154- } ,
155- { } as Record < string , Connection [ ] > ,
156- ) ;
157- }
158-
159139 private getRelationshipDirection ( connection : Connection , startNodeId : string ) : 'OUTGOING' | 'INCOMING' | 'UNKNOWN' {
160140 // Check the first relationship in the chain to determine direction from start node
161141 const firstRel = connection . relationshipChain ?. [ 0 ] as any ;
@@ -190,29 +170,34 @@ export class TraversalHandler {
190170 // JSON:API normalization - collect all unique nodes
191171 const nodeMap = new Map < string , any > ( ) ;
192172
173+ // Get common root path from all nodes
174+ const allNodes = [ startNode , ...traversalData . connections . map ( ( c ) => c . node ) ] ;
175+ const projectRoot = this . getCommonRootPath ( allNodes ) ;
176+
193177 // Add start node to map
194178 if ( includeStartNodeDetails ) {
195- const startNodeData = this . formatNodeJSON ( startNode , includeCode , snippetLength ) ;
179+ const startNodeData = this . formatNodeJSON ( startNode , includeCode , snippetLength , projectRoot ) ;
196180 nodeMap . set ( startNode . properties . id , startNodeData ) ;
197181 }
198182
199183 // Collect all unique nodes from connections
200184 traversalData . connections . forEach ( ( conn ) => {
201185 const nodeId = conn . node . properties . id ;
202186 if ( ! nodeMap . has ( nodeId ) ) {
203- nodeMap . set ( nodeId , this . formatNodeJSON ( conn . node , includeCode , snippetLength ) ) ;
187+ nodeMap . set ( nodeId , this . formatNodeJSON ( conn . node , includeCode , snippetLength , projectRoot ) ) ;
204188 }
205189 } ) ;
206190
207191 const byDepth = this . groupConnectionsByDepth ( traversalData . connections ) ;
208192
209193 return {
194+ projectRoot,
210195 totalConnections : traversalData . connections . length ,
211196 uniqueFiles : this . getUniqueFileCount ( traversalData . connections ) ,
212197 maxDepth : Object . keys ( byDepth ) . length > 0 ? Math . max ( ...Object . keys ( byDepth ) . map ( ( d ) => parseInt ( d ) ) ) : 0 ,
213198 startNodeId : includeStartNodeDetails ? startNode . properties . id : undefined ,
214199 nodes : Object . fromEntries ( nodeMap ) ,
215- depths : this . formatConnectionsByDepthWithReferences ( byDepth , maxNodesPerChain , startNode . properties . id ) ,
200+ depths : this . formatConnectionsByDepthWithReferences ( byDepth , maxNodesPerChain ) ,
216201 } ;
217202 }
218203
@@ -227,36 +212,43 @@ export class TraversalHandler {
227212 Object . keys ( byDepth ) . length > 0 ? Math . max ( ...Object . keys ( byDepth ) . map ( ( d ) => parseInt ( d ) ) ) : 0 ;
228213 const uniqueFiles = this . getUniqueFileCount ( traversalData . connections ) ;
229214
215+ const allNodes = [ startNode , ...traversalData . connections . map ( ( c ) => c . node ) ] ;
216+ const projectRoot = this . getCommonRootPath ( allNodes ) ;
217+
230218 const fileMap = new Map < string , number > ( ) ;
231219 traversalData . connections . forEach ( ( conn ) => {
232220 const filePath = conn . node . properties . filePath ;
233221 if ( filePath ) {
234- fileMap . set ( filePath , ( fileMap . get ( filePath ) ?? 0 ) + 1 ) ;
222+ const relativePath = this . makeRelativePath ( filePath , projectRoot ) ;
223+ fileMap . set ( relativePath , ( fileMap . get ( relativePath ) ?? 0 ) + 1 ) ;
235224 }
236225 } ) ;
237226
238227 const connectedFiles = Array . from ( fileMap . entries ( ) )
239228 . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
240229 . map ( ( [ file , count ] ) => ( { file, nodeCount : count } ) ) ;
241230
231+ const maxSummaryFiles = DEFAULTS . maxResultsDisplayed ;
232+
242233 return {
234+ projectRoot,
243235 startNodeId : startNode . properties . id ,
244236 nodes : {
245- [ startNode . properties . id ] : this . formatNodeJSON ( startNode , false , 0 ) ,
237+ [ startNode . properties . id ] : this . formatNodeJSON ( startNode , false , 0 , projectRoot ) ,
246238 } ,
247239 totalConnections,
248240 maxDepth : maxDepthFound ,
249241 uniqueFiles,
250- files : connectedFiles . slice ( 0 , 20 ) ,
251- ...( fileMap . size > 20 && { hasMore : fileMap . size - 20 } ) ,
242+ files : connectedFiles . slice ( 0 , maxSummaryFiles ) ,
243+ ...( fileMap . size > maxSummaryFiles && { hasMore : fileMap . size - maxSummaryFiles } ) ,
252244 } ;
253245 }
254246
255- private formatNodeJSON ( node : Neo4jNode , includeCode : boolean , snippetLength : number ) : any {
247+ private formatNodeJSON ( node : Neo4jNode , includeCode : boolean , snippetLength : number , projectRoot ?: string ) : any {
256248 const result : any = {
257249 id : node . properties . id ,
258- type : node . labels [ 0 ] ?? 'Unknown' ,
259- filePath : node . properties . filePath ,
250+ type : node . properties . semanticType ?? node . labels . at ( - 1 ) ?? 'Unknown' ,
251+ filePath : projectRoot ? this . makeRelativePath ( node . properties . filePath , projectRoot ) : node . properties . filePath ,
260252 } ;
261253
262254 if ( node . properties . name ) {
@@ -265,14 +257,15 @@ export class TraversalHandler {
265257
266258 if ( includeCode && node . properties . sourceCode && node . properties . coreType !== 'SourceFile' ) {
267259 const code = node . properties . sourceCode ;
268- const maxLength = 1000 ; // Show max 1000 chars total
260+ const maxLength = snippetLength ; // Use the provided snippet length
269261
270262 if ( code . length <= maxLength ) {
271263 result . sourceCode = code ;
272264 } else {
273- // Show first 500 and last 500 characters
265+ // Show first half and last half of the snippet
274266 const half = Math . floor ( maxLength / 2 ) ;
275- result . sourceCode = code . substring ( 0 , half ) + '\n\n... [truncated] ...\n\n' + code . substring ( code . length - half ) ;
267+ result . sourceCode =
268+ code . substring ( 0 , half ) + '\n\n... [truncated] ...\n\n' + code . substring ( code . length - half ) ;
276269 result . hasMore = true ;
277270 result . truncated = code . length - maxLength ;
278271 }
@@ -284,38 +277,70 @@ export class TraversalHandler {
284277 private formatConnectionsByDepthWithReferences (
285278 byDepth : Record < number , Connection [ ] > ,
286279 maxNodesPerChain : number ,
287- startNodeId : string ,
288280 ) : any [ ] {
289281 return Object . keys ( byDepth )
290282 . sort ( ( a , b ) => parseInt ( a ) - parseInt ( b ) )
291283 . map ( ( depth ) => {
292284 const depthConnections = byDepth [ parseInt ( depth ) ] ;
293- const byRelChain = this . groupConnectionsByRelationshipChain ( depthConnections ) ;
294-
295- const chains = Object . entries ( byRelChain ) . map ( ( [ chain , nodes ] ) => {
296- const firstNode = nodes [ 0 ] ;
297- const direction = this . getRelationshipDirection ( firstNode , startNodeId ) ;
298- const displayNodes = nodes . slice ( 0 , maxNodesPerChain ) ;
299-
300- const chainResult : any = {
301- via : chain ,
302- direction,
303- count : nodes . length ,
304- nodeIds : displayNodes . map ( ( conn ) => conn . node . properties . id ) ,
305- } ;
306285
307- if ( nodes . length > maxNodesPerChain ) {
308- chainResult . hasMore = nodes . length - maxNodesPerChain ;
309- }
286+ const connectionsToShow = Math . min ( depthConnections . length , maxNodesPerChain ) ;
310287
311- return chainResult ;
288+ const chains = depthConnections . slice ( 0 , connectionsToShow ) . map ( ( conn ) => {
289+ return (
290+ conn . relationshipChain ?. map ( ( rel : any ) => ( {
291+ type : rel . type ,
292+ from : rel . start ,
293+ to : rel . end ,
294+ } ) ) ?? [ ]
295+ ) ;
312296 } ) ;
313297
314298 return {
315299 depth : parseInt ( depth ) ,
316300 count : depthConnections . length ,
317301 chains,
302+ ...( depthConnections . length > connectionsToShow && {
303+ hasMore : depthConnections . length - connectionsToShow ,
304+ } ) ,
318305 } ;
319306 } ) ;
320307 }
308+
309+ private getCommonRootPath ( nodes : Neo4jNode [ ] ) : string {
310+ const filePaths = nodes . map ( ( n ) => n . properties . filePath ) . filter ( Boolean ) as string [ ] ;
311+
312+ if ( filePaths . length === 0 ) return process . cwd ( ) ;
313+
314+ // Split all paths into parts
315+ const pathParts = filePaths . map ( ( p ) => p . split ( '/' ) ) ;
316+
317+ // Find common prefix
318+ const commonParts : string [ ] = [ ] ;
319+ const firstPath = pathParts [ 0 ] ;
320+
321+ for ( let i = 0 ; i < firstPath . length ; i ++ ) {
322+ const part = firstPath [ i ] ;
323+ if ( pathParts . every ( ( p ) => p [ i ] === part ) ) {
324+ commonParts . push ( part ) ;
325+ } else {
326+ break ;
327+ }
328+ }
329+
330+ return commonParts . join ( '/' ) || '/' ;
331+ }
332+
333+ private makeRelativePath ( absolutePath : string | undefined , projectRoot : string ) : string {
334+ if ( ! absolutePath ) return '' ;
335+ if ( ! projectRoot || projectRoot === '/' ) return absolutePath ;
336+
337+ // Ensure both paths end consistently
338+ const root = projectRoot . endsWith ( '/' ) ? projectRoot : projectRoot + '/' ;
339+
340+ if ( absolutePath . startsWith ( root ) ) {
341+ return absolutePath . substring ( root . length ) ;
342+ }
343+
344+ return absolutePath ;
345+ }
321346}
0 commit comments