diff --git a/internal/engine/engine.go b/internal/engine/engine.go index c8b5f37..a70e4df 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -54,7 +54,7 @@ var purposeMap = map[string]string{ "lib": "Shared library code", "utils": "Utility functions", "services": "Business logic services", - "middleware": "HTTP middleware", + "middleware": "HTTP middleware", "cli": "Command-line interface", "schema": "Data schema definitions", "renderer": "Output renderers", @@ -108,6 +108,46 @@ func generateAddFeatureHint(modules map[string]schema.ModuleInfo, entrypoints [] return "" } +func filterRetainedModules(names []string, retained map[string]bool) []string { + if len(names) == 0 { + return nil + } + out := make([]string, 0, len(names)) + for _, name := range names { + if retained[name] { + out = append(out, name) + } + } + return out +} + +func rankMostDepended(modules map[string]schema.ModuleInfo) []string { + names := make([]string, 0, len(modules)) + for name := range modules { + names = append(names, name) + } + sort.SliceStable(names, func(i, j int) bool { + ci := len(modules[names[i]].DependedBy) + cj := len(modules[names[j]].DependedBy) + if ci != cj { + return ci > cj + } + return names[i] < names[j] + }) + return names +} + +func findIsolatedModules(modules map[string]schema.ModuleInfo) []string { + var isolated []string + for name, mod := range modules { + if len(mod.DependsOn) == 0 && len(mod.DependedBy) == 0 { + isolated = append(isolated, name) + } + } + sort.Strings(isolated) + return isolated +} + // detectDoNotTouch returns paths that should generally not be modified by hand. func detectDoNotTouch(root string) []string { candidates := []string{"migrations", "vendor", "generated", "proto", ".github"} @@ -439,6 +479,14 @@ func assembleIndex( allMods = allMods[:maxModules] } + retainedModules := make(map[string]bool, len(allMods)) + for _, mod := range allMods { + if strings.HasPrefix(mod.Name, "testdata") { + continue + } + retainedModules[mod.Name] = true + } + modules := map[string]schema.ModuleInfo{} for _, mod := range allMods { if strings.HasPrefix(mod.Name, "testdata") { @@ -491,8 +539,8 @@ func assembleIndex( FileList: fileList, Exports: exports, TypeDefs: typeDefs, - DependsOn: mod.DependsOn, - DependedBy: mod.DependedBy, + DependsOn: filterRetainedModules(mod.DependsOn, retainedModules), + DependedBy: filterRetainedModules(mod.DependedBy, retainedModules), Activity: activityLevel, } } @@ -504,14 +552,17 @@ func assembleIndex( if strings.HasPrefix(e.From, "testdata") || strings.HasPrefix(e.To, "testdata") { continue } + if !retainedModules[e.From] || !retainedModules[e.To] { + continue + } schemaEdges = append(schemaEdges, [2]string{e.From, e.To}) } - mostDepended := g.MostDepended() + mostDepended := rankMostDepended(modules) // Cap most-depended list. if len(mostDepended) > 10 { mostDepended = mostDepended[:10] } - isolated := g.Isolated() + isolated := findIsolatedModules(modules) // --- Git --- hotFiles := make([]schema.HotFile, len(activity.HotFiles)) diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go new file mode 100644 index 0000000..e7c1fa2 --- /dev/null +++ b/internal/engine/engine_test.go @@ -0,0 +1,74 @@ +package engine + +import ( + "testing" + + "github.com/glincker/stacklit/internal/config" + "github.com/glincker/stacklit/internal/git" + "github.com/glincker/stacklit/internal/graph" + "github.com/glincker/stacklit/internal/monorepo" + "github.com/glincker/stacklit/internal/parser" +) + +func TestAssembleIndexFiltersTrimmedModuleReferences(t *testing.T) { + files := []*parser.FileInfo{ + {Path: "src/api/index.ts", Language: "TypeScript", Imports: []string{"src/auth", "src/db"}, LineCount: 50}, + {Path: "src/auth/service.ts", Language: "TypeScript", Imports: []string{"src/db"}, LineCount: 40}, + {Path: "src/db/pool.ts", Language: "TypeScript", Exports: []string{"Pool"}, LineCount: 30}, + } + + g := graph.Build(files, graph.BuildOptions{MaxDepth: 4}) + cfg := config.DefaultConfig() + cfg.MaxModules = 2 + + idx := assembleIndex( + ".", + &monorepo.Result{Type: "single"}, + []string{"src/api/index.ts", "src/auth/service.ts", "src/db/pool.ts"}, + files, + g, + &git.Activity{}, + map[string][]byte{}, + cfg, + ) + + if len(idx.Modules) != 2 { + t.Fatalf("expected 2 retained modules, got %d: %v", len(idx.Modules), idx.Modules) + } + if _, ok := idx.Modules["src/db"]; ok { + t.Fatalf("expected trimmed module src/db to be omitted from modules, got %v", idx.Modules) + } + + api := idx.Modules["src/api"] + if len(api.DependsOn) != 1 || api.DependsOn[0] != "src/auth" { + t.Fatalf("expected src/api depends_on to retain only src/auth, got %v", api.DependsOn) + } + + auth := idx.Modules["src/auth"] + if len(auth.DependsOn) != 0 { + t.Fatalf("expected src/auth depends_on to drop trimmed src/db reference, got %v", auth.DependsOn) + } + if len(auth.DependedBy) != 1 || auth.DependedBy[0] != "src/api" { + t.Fatalf("expected src/auth depended_by to contain only src/api, got %v", auth.DependedBy) + } + + if len(idx.Dependencies.Edges) != 1 || idx.Dependencies.Edges[0] != ([2]string{"src/api", "src/auth"}) { + t.Fatalf("expected only retained edge src/api -> src/auth, got %v", idx.Dependencies.Edges) + } + + if len(idx.Dependencies.MostDepended) < 2 { + t.Fatalf("expected ranked retained modules, got %v", idx.Dependencies.MostDepended) + } + if idx.Dependencies.MostDepended[0] != "src/auth" { + t.Fatalf("expected src/auth to be most depended after trimming, got %v", idx.Dependencies.MostDepended) + } + for _, name := range idx.Dependencies.MostDepended { + if name == "src/db" { + t.Fatalf("expected trimmed module src/db to be absent from most_depended, got %v", idx.Dependencies.MostDepended) + } + } + + if len(idx.Dependencies.Isolated) != 0 { + t.Fatalf("expected no isolated retained modules, got %v", idx.Dependencies.Isolated) + } +}