@@ -132,6 +132,31 @@ export class ConfigMerger {
132132 }
133133
134134 private normalizeTitle ( title : string ) : string {
135+ // Special normalization for "Development Assistant" titles to group them together
136+ if ( title . toLowerCase ( ) . includes ( 'development assistant' ) ) {
137+ return 'development assistant' ;
138+ }
139+
140+ // Special normalization for similar section types
141+ const normalizations : Record < string , string > = {
142+ 'breaking changes' : 'breaking changes' ,
143+ 'file conventions' : 'file conventions' ,
144+ 'project structure' : 'project structure' ,
145+ 'common commands' : 'common commands' ,
146+ 'available commands' : 'available commands' ,
147+ 'security best practices' : 'security best practices' ,
148+ 'performance optimization' : 'performance optimization' ,
149+ 'testing' : 'testing' ,
150+ 'deployment' : 'deployment' ,
151+ } ;
152+
153+ const titleLower = title . toLowerCase ( ) ;
154+ for ( const [ pattern , normalized ] of Object . entries ( normalizations ) ) {
155+ if ( titleLower . includes ( pattern ) ) {
156+ return normalized ;
157+ }
158+ }
159+
135160 return title
136161 . toLowerCase ( )
137162 . replace ( / [ ^ a - z 0 - 9 \s ] / g, '' )
@@ -219,17 +244,30 @@ export class ConfigMerger {
219244 try {
220245 const bestSection = this . selectBestSection ( sections ) ;
221246
247+ // Skip sections with no content
248+ const hasContent = sections . some ( s => s . content && s . content . trim ( ) !== '' ) ;
249+ if ( ! hasContent ) {
250+ processedSections . add ( key ) ;
251+ continue ;
252+ }
253+
222254 if ( this . shouldMergeSections ( sections ) ) {
223255 const merged = this . mergeSimilarSections ( sections ) ;
224- output . push ( `${ '#' . repeat ( Math . min ( bestSection . level , 2 ) ) } ${ bestSection . title } ` ) ;
225- output . push ( '' ) ;
226- output . push ( merged ) ;
227- output . push ( '' ) ;
256+ // Only add if merged content is not empty
257+ if ( merged && merged . trim ( ) ) {
258+ output . push ( `${ '#' . repeat ( Math . min ( bestSection . level , 2 ) ) } ${ bestSection . title } ` ) ;
259+ output . push ( '' ) ;
260+ output . push ( merged ) ;
261+ output . push ( '' ) ;
262+ }
228263 } else {
229- output . push ( `${ '#' . repeat ( Math . min ( bestSection . level , 2 ) ) } ${ bestSection . title } ` ) ;
230- output . push ( '' ) ;
231- output . push ( bestSection . content ) ;
232- output . push ( '' ) ;
264+ // Only add if content is not empty
265+ if ( bestSection . content && bestSection . content . trim ( ) ) {
266+ output . push ( `${ '#' . repeat ( Math . min ( bestSection . level , 2 ) ) } ${ bestSection . title } ` ) ;
267+ output . push ( '' ) ;
268+ output . push ( bestSection . content ) ;
269+ output . push ( '' ) ;
270+ }
233271 }
234272
235273 processedSections . add ( key ) ;
@@ -355,39 +393,206 @@ export class ConfigMerger {
355393 const mergedContent : string [ ] = [ ] ;
356394 const sources = [ ...new Set ( sections . map ( s => s . source ) ) ] ;
357395
358- mergedContent . push ( `*Combined from: ${ sources . join ( ', ' ) } *` ) ;
359- mergedContent . push ( '' ) ;
396+ // Only add "Combined from" if there are multiple unique sources
397+ if ( sources . length > 1 ) {
398+ mergedContent . push ( `*Combined from: ${ sources . join ( ', ' ) } *` ) ;
399+ mergedContent . push ( '' ) ;
400+ }
401+
402+ // Special handling for sections with numbered lists (like Security Best Practices)
403+ const isNumberedListSection = sections . some ( s =>
404+ s . content . match ( / ^ \d + \. \s + / m) || s . content . includes ( '1. ' )
405+ ) ;
406+
407+ if ( isNumberedListSection ) {
408+ return this . mergeNumberedLists ( sections , sources ) ;
409+ }
410+
411+ // Special handling for project context sections
412+ if ( sections [ 0 ] . title . toLowerCase ( ) . includes ( 'project context' ) ) {
413+ return this . mergeProjectContexts ( sections , sources ) ;
414+ }
360415
361- const contentMap = new Map < string , Set < string > > ( ) ;
416+ // For other sections, use improved content merging
417+ const contentMap = new Map < string , string [ ] > ( ) ;
418+ const processedContent = new Set < string > ( ) ;
362419
363420 for ( const section of sections ) {
421+ if ( ! section . content || section . content . trim ( ) === '' ) continue ;
422+
364423 const lines = section . content . split ( '\n' ) ;
365424 let currentSubsection = 'main' ;
366425
367426 for ( const line of lines ) {
368- if ( line . startsWith ( '###' ) ) {
427+ // Track subsections
428+ if ( line . match ( / ^ # { 3 , } \s + / ) ) {
369429 currentSubsection = line ;
370430 }
371431
372432 if ( ! contentMap . has ( currentSubsection ) ) {
373- contentMap . set ( currentSubsection , new Set ( ) ) ;
433+ contentMap . set ( currentSubsection , [ ] ) ;
374434 }
375435
376436 const normalizedLine = line . trim ( ) ;
377- if ( normalizedLine && ! normalizedLine . startsWith ( '*Combined from:' ) ) {
378- contentMap . get ( currentSubsection ) ! . add ( line ) ;
437+ const contentKey = normalizedLine . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] / g, '' ) ;
438+
439+ // Skip empty lines and duplicates
440+ if ( normalizedLine &&
441+ ! normalizedLine . startsWith ( '*Combined from:' ) &&
442+ ! processedContent . has ( contentKey ) ) {
443+ contentMap . get ( currentSubsection ) ! . push ( line ) ;
444+ if ( contentKey ) processedContent . add ( contentKey ) ;
379445 }
380446 }
381447 }
382448
449+ // Rebuild content with proper structure
383450 for ( const [ subsection , lines ] of contentMap ) {
451+ if ( lines . length === 0 ) continue ; // Skip empty subsections
452+
384453 if ( subsection !== 'main' ) {
385454 mergedContent . push ( subsection ) ;
386455 }
387- mergedContent . push ( ...Array . from ( lines ) ) ;
456+ mergedContent . push ( ...lines ) ;
457+ }
458+
459+ return mergedContent . join ( '\n' ) . trim ( ) ;
460+ }
461+
462+ private mergeNumberedLists ( sections : Section [ ] , sources : string [ ] ) : string {
463+ const mergedContent : string [ ] = [ ] ;
464+
465+ if ( sources . length > 1 ) {
466+ mergedContent . push ( `*Combined from: ${ sources . join ( ', ' ) } *` ) ;
467+ mergedContent . push ( '' ) ;
468+ }
469+
470+ const allItems = new Map < string , { content : string ; source : string } > ( ) ;
471+ let itemNumber = 1 ;
472+
473+ for ( const section of sections ) {
474+ if ( ! section . content || section . content . trim ( ) === '' ) continue ;
475+
476+ const lines = section . content . split ( '\n' ) ;
477+ let currentItem : string [ ] = [ ] ;
478+ let isInItem = false ;
479+
480+ for ( const line of lines ) {
481+ // Check if this is a numbered item (at any level)
482+ if ( line . match ( / ^ \d + \. \s + / ) ) {
483+ // Save previous item if exists
484+ if ( currentItem . length > 0 ) {
485+ const itemText = currentItem . join ( '\n' ) ;
486+ const itemKey = itemText . toLowerCase ( ) . replace ( / ^ \d + \. \s + / , '' ) . trim ( ) ;
487+
488+ if ( ! allItems . has ( itemKey ) ) {
489+ allItems . set ( itemKey , { content : itemText , source : section . source } ) ;
490+ }
491+ }
492+
493+ currentItem = [ line ] ;
494+ isInItem = true ;
495+ } else if ( isInItem && line . match ( / ^ \s + / ) ) {
496+ // Continuation of current item (indented)
497+ currentItem . push ( line ) ;
498+ } else if ( line . trim ( ) === '' ) {
499+ // Empty line might end an item
500+ if ( currentItem . length > 0 ) {
501+ const itemText = currentItem . join ( '\n' ) ;
502+ const itemKey = itemText . toLowerCase ( ) . replace ( / ^ \d + \. \s + / , '' ) . trim ( ) ;
503+
504+ if ( ! allItems . has ( itemKey ) ) {
505+ allItems . set ( itemKey , { content : itemText , source : section . source } ) ;
506+ }
507+ currentItem = [ ] ;
508+ isInItem = false ;
509+ }
510+ } else if ( ! line . startsWith ( '*Combined from:' ) ) {
511+ // Other content
512+ if ( currentItem . length > 0 ) {
513+ const itemText = currentItem . join ( '\n' ) ;
514+ const itemKey = itemText . toLowerCase ( ) . replace ( / ^ \d + \. \s + / , '' ) . trim ( ) ;
515+
516+ if ( ! allItems . has ( itemKey ) ) {
517+ allItems . set ( itemKey , { content : itemText , source : section . source } ) ;
518+ }
519+ currentItem = [ ] ;
520+ isInItem = false ;
521+ }
522+ mergedContent . push ( line ) ;
523+ }
524+ }
525+
526+ // Don't forget the last item
527+ if ( currentItem . length > 0 ) {
528+ const itemText = currentItem . join ( '\n' ) ;
529+ const itemKey = itemText . toLowerCase ( ) . replace ( / ^ \d + \. \s + / , '' ) . trim ( ) ;
530+
531+ if ( ! allItems . has ( itemKey ) ) {
532+ allItems . set ( itemKey , { content : itemText , source : section . source } ) ;
533+ }
534+ }
535+ }
536+
537+ // Renumber and add all unique items
538+ for ( const { content } of allItems . values ( ) ) {
539+ const renumbered = content . replace ( / ^ \d + \. \s + / , `${ itemNumber } . ` ) ;
540+ mergedContent . push ( renumbered ) ;
541+ itemNumber ++ ;
542+ }
543+
544+ return mergedContent . join ( '\n' ) . trim ( ) ;
545+ }
546+
547+ private mergeProjectContexts ( sections : Section [ ] , sources : string [ ] ) : string {
548+ const mergedContent : string [ ] = [ ] ;
549+
550+ if ( sources . length > 1 ) {
551+ mergedContent . push ( `*Combined from: ${ sources . join ( ', ' ) } *` ) ;
552+ mergedContent . push ( '' ) ;
553+ }
554+
555+ // Collect unique project descriptions
556+ const projectDescriptions = new Map < string , string > ( ) ;
557+
558+ for ( const section of sections ) {
559+ if ( ! section . content || section . content . trim ( ) === '' ) continue ;
560+
561+ // Extract project description paragraphs
562+ const lines = section . content . split ( '\n' ) ;
563+ const descriptionLines : string [ ] = [ ] ;
564+
565+ for ( const line of lines ) {
566+ if ( ! line . startsWith ( '*Combined from:' ) ) {
567+ descriptionLines . push ( line ) ;
568+ }
569+ }
570+
571+ if ( descriptionLines . length > 0 ) {
572+ const description = descriptionLines . join ( '\n' ) . trim ( ) ;
573+ if ( description && ! projectDescriptions . has ( section . source ) ) {
574+ projectDescriptions . set ( section . source , description ) ;
575+ }
576+ }
577+ }
578+
579+ // Combine project descriptions intelligently
580+ const descriptions = Array . from ( projectDescriptions . values ( ) ) ;
581+ if ( descriptions . length === 1 ) {
582+ mergedContent . push ( descriptions [ 0 ] ) ;
583+ } else {
584+ // For multiple descriptions, present them as a unified context
585+ mergedContent . push ( 'This is a comprehensive project that combines multiple technologies:' ) ;
586+ mergedContent . push ( '' ) ;
587+
588+ for ( const desc of descriptions ) {
589+ // Add each description as a paragraph
590+ mergedContent . push ( desc ) ;
591+ mergedContent . push ( '' ) ;
592+ }
388593 }
389594
390- return mergedContent . join ( '\n' ) ;
595+ return mergedContent . join ( '\n' ) . trim ( ) ;
391596 }
392597
393598 private createEmptyConfiguration ( ) : string {
0 commit comments