@@ -177,7 +177,7 @@ describe("analyzeGraph", () => {
177177 expect ( result . forceAnalysis . moduleCohesion . length ) . toBeGreaterThan ( 0 ) ;
178178
179179 const verdicts = result . forceAnalysis . moduleCohesion . map ( ( m ) => m . verdict ) ;
180- expect ( verdicts . every ( ( v ) => [ "COHESIVE" , "MODERATE" , "JUNK_DRAWER" ] . includes ( v ) ) ) . toBe ( true ) ;
180+ expect ( verdicts . every ( ( v ) => [ "COHESIVE" , "MODERATE" , "JUNK_DRAWER" , "LEAF" ] . includes ( v ) ) ) . toBe ( true ) ;
181181 } ) ;
182182
183183 it ( "detects tension files pulled by multiple modules" , ( ) => {
@@ -544,3 +544,210 @@ describe("computeGroups", () => {
544544 }
545545 } ) ;
546546} ) ;
547+
548+ describe ( "LEAF verdict for single-file modules" , ( ) => {
549+ it ( "AC-1: single non-test file module gets LEAF verdict" , ( ) => {
550+ const files = [
551+ makeFile ( "src/search/index.ts" ) ,
552+ makeFile ( "src/parser/a.ts" , { imports : [ imp ( "src/parser/b.ts" ) ] } ) ,
553+ makeFile ( "src/parser/b.ts" ) ,
554+ ] ;
555+ const built = buildGraph ( files ) ;
556+ const result = analyzeGraph ( built , files ) ;
557+
558+ const searchMod = result . forceAnalysis . moduleCohesion . find ( ( m ) => m . path === "src/search/" ) ;
559+ expect ( searchMod ) . toBeDefined ( ) ;
560+ expect ( searchMod ?. verdict ) . toBe ( "LEAF" ) ;
561+ } ) ;
562+
563+ it ( "AC-E2: 2-file module with 0 internal deps gets JUNK_DRAWER, not LEAF" , ( ) => {
564+ const files = [
565+ makeFile ( "src/grab/a.ts" , { imports : [ imp ( "src/other/x.ts" ) ] } ) ,
566+ makeFile ( "src/grab/b.ts" , { imports : [ imp ( "src/other/y.ts" ) ] } ) ,
567+ makeFile ( "src/other/x.ts" ) ,
568+ makeFile ( "src/other/y.ts" ) ,
569+ ] ;
570+ const built = buildGraph ( files ) ;
571+ const result = analyzeGraph ( built , files ) ;
572+
573+ const grabMod = result . forceAnalysis . moduleCohesion . find ( ( m ) => m . path === "src/grab/" ) ;
574+ expect ( grabMod ) . toBeDefined ( ) ;
575+ expect ( grabMod ?. verdict ) . toBe ( "JUNK_DRAWER" ) ;
576+ } ) ;
577+
578+ it ( "EC1: 1-file 0-dep module gets LEAF" , ( ) => {
579+ const files = [
580+ makeFile ( "src/lonely/index.ts" ) ,
581+ ] ;
582+ const built = buildGraph ( files ) ;
583+ const result = analyzeGraph ( built , files ) ;
584+
585+ const mod = result . forceAnalysis . moduleCohesion . find ( ( m ) => m . path === "src/lonely/" ) ;
586+ expect ( mod ) . toBeDefined ( ) ;
587+ expect ( mod ?. verdict ) . toBe ( "LEAF" ) ;
588+ } ) ;
589+
590+ it ( "EC2: 1-file module with outgoing deps still gets LEAF" , ( ) => {
591+ const files = [
592+ makeFile ( "src/single/index.ts" , { imports : [ imp ( "src/other/a.ts" ) , imp ( "src/other/b.ts" ) ] } ) ,
593+ makeFile ( "src/other/a.ts" ) ,
594+ makeFile ( "src/other/b.ts" ) ,
595+ ] ;
596+ const built = buildGraph ( files ) ;
597+ const result = analyzeGraph ( built , files ) ;
598+
599+ const singleMod = result . forceAnalysis . moduleCohesion . find ( ( m ) => m . path === "src/single/" ) ;
600+ expect ( singleMod ) . toBeDefined ( ) ;
601+ expect ( singleMod ?. verdict ) . toBe ( "LEAF" ) ;
602+ } ) ;
603+
604+ it ( "AC-10/EC6: module with 1 prod file + 1 test file gets LEAF" , ( ) => {
605+ const files = [
606+ makeFile ( "src/community/index.ts" ) ,
607+ makeFile ( "src/community/index.test.ts" , { isTestFile : true } ) ,
608+ ] ;
609+ const built = buildGraph ( files ) ;
610+ const result = analyzeGraph ( built , files ) ;
611+
612+ const mod = result . forceAnalysis . moduleCohesion . find ( ( m ) => m . path === "src/community/" ) ;
613+ expect ( mod ) . toBeDefined ( ) ;
614+ expect ( mod ?. verdict ) . toBe ( "LEAF" ) ;
615+ } ) ;
616+
617+ it ( "EC7: module with 2 prod files + 1 test file is NOT LEAF" , ( ) => {
618+ const files = [
619+ makeFile ( "src/mod/a.ts" , { imports : [ imp ( "src/other/x.ts" ) ] } ) ,
620+ makeFile ( "src/mod/b.ts" , { imports : [ imp ( "src/other/y.ts" ) ] } ) ,
621+ makeFile ( "src/mod/a.test.ts" , { isTestFile : true } ) ,
622+ makeFile ( "src/other/x.ts" ) ,
623+ makeFile ( "src/other/y.ts" ) ,
624+ ] ;
625+ const built = buildGraph ( files ) ;
626+ const result = analyzeGraph ( built , files ) ;
627+
628+ const mod = result . forceAnalysis . moduleCohesion . find ( ( m ) => m . path === "src/mod/" ) ;
629+ expect ( mod ) . toBeDefined ( ) ;
630+ expect ( mod ?. verdict ) . not . toBe ( "LEAF" ) ;
631+ } ) ;
632+
633+ it ( "AC-6: summary does not count LEAF modules as junk-drawer" , ( ) => {
634+ const files = [
635+ makeFile ( "src/search/index.ts" ) ,
636+ makeFile ( "src/grab/a.ts" , { imports : [ imp ( "src/other/x.ts" ) ] } ) ,
637+ makeFile ( "src/grab/b.ts" , { imports : [ imp ( "src/other/y.ts" ) ] } ) ,
638+ makeFile ( "src/other/x.ts" ) ,
639+ makeFile ( "src/other/y.ts" ) ,
640+ ] ;
641+ const built = buildGraph ( files ) ;
642+ const result = analyzeGraph ( built , files ) ;
643+
644+ // search/ is LEAF and should NOT be counted in junk-drawer summary
645+ expect ( result . forceAnalysis . summary ) . not . toContain ( "src/search/" ) ;
646+ // grab/ is JUNK_DRAWER and should be in summary
647+ expect ( result . forceAnalysis . summary ) . toContain ( "src/grab/" ) ;
648+ } ) ;
649+ } ) ;
650+
651+ describe ( "tension suppression for type hubs and entry points" , ( ) => {
652+ it ( "AC-2: type hub file gets suppressed split recommendation" , ( ) => {
653+ // types/index.ts is pulled by two different modules
654+ const files = [
655+ makeFile ( "src/types/index.ts" , {
656+ imports : [ imp ( "src/a/x.ts" ) , imp ( "src/b/y.ts" ) ] ,
657+ exports : [
658+ { name : "MyType" , type : "interface" , loc : 1 , isDefault : false , complexity : 1 } ,
659+ ] ,
660+ } ) ,
661+ makeFile ( "src/a/x.ts" ) ,
662+ makeFile ( "src/b/y.ts" ) ,
663+ ] ;
664+ const built = buildGraph ( files ) ;
665+ const result = analyzeGraph ( built , files ) ;
666+
667+ const typesFile = result . forceAnalysis . tensionFiles . find (
668+ ( t ) => t . file === "src/types/index.ts"
669+ ) ;
670+ if ( typesFile ) {
671+ expect ( typesFile . recommendation ) . toContain ( "not recommended" ) ;
672+ expect ( typesFile . recommendation ) . not . toContain ( "Split into" ) ;
673+ }
674+ } ) ;
675+
676+ it ( "AC-3: entry point file gets suppressed split recommendation" , ( ) => {
677+ // cli.ts is pulled by two different modules
678+ const files = [
679+ makeFile ( "cli.ts" , {
680+ imports : [ imp ( "src/a/x.ts" ) , imp ( "src/b/y.ts" ) ] ,
681+ } ) ,
682+ makeFile ( "src/a/x.ts" ) ,
683+ makeFile ( "src/b/y.ts" ) ,
684+ ] ;
685+ const built = buildGraph ( files ) ;
686+ const result = analyzeGraph ( built , files ) ;
687+
688+ const cliFile = result . forceAnalysis . tensionFiles . find (
689+ ( t ) => t . file === "cli.ts"
690+ ) ;
691+ if ( cliFile ) {
692+ expect ( cliFile . recommendation ) . toContain ( "not recommended" ) ;
693+ expect ( cliFile . recommendation ) . not . toContain ( "Split into" ) ;
694+ }
695+ } ) ;
696+
697+ it ( "EC4: types.ts in nested module gets suppressed split rec" , ( ) => {
698+ const files = [
699+ makeFile ( "src/core/types.ts" , {
700+ imports : [ imp ( "src/a/x.ts" ) , imp ( "src/b/y.ts" ) ] ,
701+ } ) ,
702+ makeFile ( "src/a/x.ts" ) ,
703+ makeFile ( "src/b/y.ts" ) ,
704+ ] ;
705+ const built = buildGraph ( files ) ;
706+ const result = analyzeGraph ( built , files ) ;
707+
708+ const typesFile = result . forceAnalysis . tensionFiles . find (
709+ ( t ) => t . file === "src/core/types.ts"
710+ ) ;
711+ if ( typesFile ) {
712+ expect ( typesFile . recommendation ) . toContain ( "not recommended" ) ;
713+ }
714+ } ) ;
715+
716+ it ( "EC5: entry point at root (main.ts, server.ts) gets suppressed" , ( ) => {
717+ const files = [
718+ makeFile ( "main.ts" , {
719+ imports : [ imp ( "src/a/x.ts" ) , imp ( "src/b/y.ts" ) ] ,
720+ } ) ,
721+ makeFile ( "src/a/x.ts" ) ,
722+ makeFile ( "src/b/y.ts" ) ,
723+ ] ;
724+ const built = buildGraph ( files ) ;
725+ const result = analyzeGraph ( built , files ) ;
726+
727+ const mainFile = result . forceAnalysis . tensionFiles . find (
728+ ( t ) => t . file === "main.ts"
729+ ) ;
730+ if ( mainFile ) {
731+ expect ( mainFile . recommendation ) . toContain ( "not recommended" ) ;
732+ }
733+ } ) ;
734+
735+ it ( "regular file with tension still gets split recommendation" , ( ) => {
736+ const files = [
737+ makeFile ( "utils.ts" , {
738+ imports : [ imp ( "src/a/x.ts" ) , imp ( "src/b/y.ts" ) ] ,
739+ } ) ,
740+ makeFile ( "src/a/x.ts" ) ,
741+ makeFile ( "src/b/y.ts" ) ,
742+ ] ;
743+ const built = buildGraph ( files ) ;
744+ const result = analyzeGraph ( built , files ) ;
745+
746+ const utilsFile = result . forceAnalysis . tensionFiles . find (
747+ ( t ) => t . file === "utils.ts"
748+ ) ;
749+ if ( utilsFile ) {
750+ expect ( utilsFile . recommendation ) . toContain ( "Split into" ) ;
751+ }
752+ } ) ;
753+ } ) ;
0 commit comments