diff --git a/internal/find/doc.graph.go b/internal/find/doc.graph.go new file mode 100644 index 0000000..c1044ed --- /dev/null +++ b/internal/find/doc.graph.go @@ -0,0 +1,14 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imported-by cmd/find.go +// imported-by internal/find/integration_test.go +// [impact] +// risk MEDIUM +// domains CLIInfrastructure · SupermodelAPI +// direct 2 +// transitive 3 +// affects cmd/find.go · internal/find/integration_test.go diff --git a/internal/find/handler.go b/internal/find/handler.go index ea26527..f099fd0 100644 --- a/internal/find/handler.go +++ b/internal/find/handler.go @@ -8,8 +8,8 @@ import ( "sort" "strings" + "github.com/supermodeltools/cli/internal/analyze" "github.com/supermodeltools/cli/internal/api" - "github.com/supermodeltools/cli/internal/cache" "github.com/supermodeltools/cli/internal/config" "github.com/supermodeltools/cli/internal/ui" ) @@ -60,21 +60,17 @@ func search(g *api.Graph, symbol, kind string) []Match { for _, rel := range rels { switch rel.Type { - case "CALLS", "CONTAINS_CALL": - if n := nodeByID[rel.EndNode]; n != nil { - callerNode := nodeByID[rel.StartNode] - if callerNode != nil { - callers[rel.EndNode] = append(callers[rel.EndNode], callerNode.Prop("name", "qualifiedName")) - } + case "calls", "contains_call": + if callerNode := nodeByID[rel.StartNode]; callerNode != nil { + callers[rel.EndNode] = append(callers[rel.EndNode], callerNode.Prop("name", "qualifiedName")) } - if n := nodeByID[rel.StartNode]; n != nil { - calleeNode := nodeByID[rel.EndNode] - if calleeNode != nil { - callees[rel.StartNode] = append(callees[rel.StartNode], calleeNode.Prop("name", "qualifiedName")) - } + if calleeNode := nodeByID[rel.EndNode]; calleeNode != nil { + callees[rel.StartNode] = append(callees[rel.StartNode], calleeNode.Prop("name", "qualifiedName")) + } + case "defines_function", "defines", "declares_class": + if fileNode := nodeByID[rel.StartNode]; fileNode != nil { + defFile[rel.EndNode] = fileNode.Prop("filePath", "path", "name") } - case "DEFINES_FUNCTION", "DEFINES", "DECLARES_CLASS": - defFile[rel.EndNode] = nodeByID[rel.StartNode].Prop("path", "name", "file") } } @@ -96,7 +92,7 @@ func search(g *api.Graph, symbol, kind string) []Match { ID: n.ID, Kind: label, Name: name, - File: n.Prop("file", "path"), + File: n.Prop("filePath", "file", "path"), DefinedIn: defFile[n.ID], } cs := callers[n.ID] @@ -139,31 +135,7 @@ func printMatches(w io.Writer, matches []Match, fmt_ ui.Format) error { return nil } -// --- Graph retrieval --------------------------------------------------------- - func getGraph(ctx context.Context, cfg *config.Config, dir string, force bool) (*api.Graph, error) { - zipPath, err := createZip(dir) - if err != nil { - return nil, err - } - defer os.Remove(zipPath) - - hash, err := cache.HashFile(zipPath) - if err != nil { - return nil, err - } - if !force { - if g, _ := cache.Get(hash); g != nil { - return g, nil - } - } - - spin := ui.Start("Analyzing repository…") - defer spin.Stop() - g, err := api.New(cfg).Analyze(ctx, zipPath, "find-"+hash[:16]) - if err != nil { - return nil, err - } - _ = cache.Put(hash, g) - return g, nil + g, _, err := analyze.GetGraph(ctx, cfg, dir, force) + return g, err } diff --git a/internal/find/handler.graph.go b/internal/find/handler.graph.go new file mode 100644 index 0000000..a74dca8 --- /dev/null +++ b/internal/find/handler.graph.go @@ -0,0 +1,43 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imports internal/api/client.go +// imports internal/api/doc.go +// imports internal/api/types.go +// imports internal/cache/cache.go +// imports internal/cache/doc.go +// imports internal/cache/fingerprint.go +// imports internal/config/config.go +// imports internal/config/doc.go +// imports internal/ui/doc.go +// imports internal/ui/output.go +// imported-by cmd/find.go +// imported-by internal/find/integration_test.go +// [calls] +// Run ← init cmd/find.go:10 +// Run ← TestIntegration_Run_Find internal/find/integration_test.go:16 +// Run ← TestIntegration_Run_Find_JSON internal/find/integration_test.go:31 +// Run ← TestIntegration_Run_Find_NoMatch internal/find/integration_test.go:47 +// Run ← TestIntegration_Run_Find_KindFilter internal/find/integration_test.go:62 +// Run → getGraph internal/find/handler.go:144 +// Run → search internal/find/handler.go:49 +// Run → printMatches internal/find/handler.go:120 +// Run → ParseFormat internal/ui/output.go:24 +// getGraph ← Run internal/find/handler.go:36 +// getGraph → Analyze internal/api/client.go:41 +// getGraph → HashFile internal/cache/cache.go:59 +// getGraph → Get internal/cache/cache.go:28 +// getGraph → Start internal/ui/output.go:74 +// getGraph → Put internal/cache/cache.go:44 +// printMatches ← Run internal/find/handler.go:36 +// printMatches → JSON internal/ui/output.go:47 +// search ← Run internal/find/handler.go:36 +// [impact] +// risk MEDIUM +// domains CLIInfrastructure · SupermodelAPI +// direct 2 +// transitive 3 +// affects cmd/find.go · internal/find/integration_test.go diff --git a/internal/find/handler_test.go b/internal/find/handler_test.go index e6dfa4f..ec971cd 100644 --- a/internal/find/handler_test.go +++ b/internal/find/handler_test.go @@ -86,8 +86,8 @@ func TestSearch_CallersAndCallees(t *testing.T) { {ID: "callee", Labels: []string{"Function"}, Properties: map[string]any{"name": "callee"}}, }, Relationships: []api.Relationship{ - {ID: "r1", Type: "CALLS", StartNode: "caller", EndNode: "target"}, - {ID: "r2", Type: "CALLS", StartNode: "target", EndNode: "callee"}, + {ID: "r1", Type: "calls", StartNode: "caller", EndNode: "target"}, + {ID: "r2", Type: "calls", StartNode: "target", EndNode: "callee"}, }, } matches := search(g, "target", "") @@ -110,7 +110,7 @@ func TestSearch_DefinesFunction(t *testing.T) { {ID: "fn1", Labels: []string{"Function"}, Properties: map[string]any{"name": "authenticate"}}, }, Relationships: []api.Relationship{ - {ID: "r1", Type: "DEFINES_FUNCTION", StartNode: "file1", EndNode: "fn1"}, + {ID: "r1", Type: "defines_function", StartNode: "file1", EndNode: "fn1"}, }, } matches := search(g, "authenticate", "Function") diff --git a/internal/find/handler_test.graph.go b/internal/find/handler_test.graph.go new file mode 100644 index 0000000..8536c4e --- /dev/null +++ b/internal/find/handler_test.graph.go @@ -0,0 +1,24 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imports internal/api/client.go +// imports internal/api/doc.go +// imports internal/api/types.go +// imports internal/ui/doc.go +// imports internal/ui/output.go +// [calls] +// TestSearch_BasicMatch → makeGraph internal/find/handler_test.go:191 +// TestSearch_CaseInsensitive → makeGraph internal/find/handler_test.go:191 +// TestSearch_KindFilter → makeGraph internal/find/handler_test.go:191 +// TestSearch_KindFilterExcludesOtherKinds → makeGraph internal/find/handler_test.go:191 +// TestSearch_NoMatch → makeGraph internal/find/handler_test.go:191 +// TestSearch_SortedByKindThenName → makeGraph internal/find/handler_test.go:191 +// makeGraph ← TestSearch_BasicMatch internal/find/handler_test.go:15 +// makeGraph ← TestSearch_CaseInsensitive internal/find/handler_test.go:28 +// makeGraph ← TestSearch_KindFilter internal/find/handler_test.go:37 +// makeGraph ← TestSearch_KindFilterExcludesOtherKinds internal/find/handler_test.go:48 +// makeGraph ← TestSearch_NoMatch internal/find/handler_test.go:59 +// makeGraph ← TestSearch_SortedByKindThenName internal/find/handler_test.go:67 diff --git a/internal/find/integration_test.graph.go b/internal/find/integration_test.graph.go new file mode 100644 index 0000000..64c13db --- /dev/null +++ b/internal/find/integration_test.graph.go @@ -0,0 +1,23 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imports internal/find/doc.go +// imports internal/find/handler.go +// imports internal/find/zip.go +// imports internal/testutil/integration.go +// [calls] +// TestIntegration_Run_Find → IntegrationConfig internal/testutil/integration.go:17 +// TestIntegration_Run_Find → MinimalGoDir internal/testutil/integration.go:102 +// TestIntegration_Run_Find → Run internal/find/handler.go:36 +// TestIntegration_Run_Find_JSON → IntegrationConfig internal/testutil/integration.go:17 +// TestIntegration_Run_Find_JSON → MinimalGoDir internal/testutil/integration.go:102 +// TestIntegration_Run_Find_JSON → Run internal/find/handler.go:36 +// TestIntegration_Run_Find_KindFilter → IntegrationConfig internal/testutil/integration.go:17 +// TestIntegration_Run_Find_KindFilter → MinimalGoDir internal/testutil/integration.go:102 +// TestIntegration_Run_Find_KindFilter → Run internal/find/handler.go:36 +// TestIntegration_Run_Find_NoMatch → IntegrationConfig internal/testutil/integration.go:17 +// TestIntegration_Run_Find_NoMatch → MinimalGoDir internal/testutil/integration.go:102 +// TestIntegration_Run_Find_NoMatch → Run internal/find/handler.go:36 diff --git a/internal/find/zip.graph.go b/internal/find/zip.graph.go new file mode 100644 index 0000000..adc9b3a --- /dev/null +++ b/internal/find/zip.graph.go @@ -0,0 +1,21 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imported-by cmd/find.go +// imported-by internal/find/integration_test.go +// [calls] +// createZip → isGitRepo internal/find/zip.go:67 +// createZip → gitArchive internal/find/zip.go:74 +// createZip → walkZip internal/find/zip.go:82 +// gitArchive ← createZip internal/find/zip.go:46 +// isGitRepo ← createZip internal/find/zip.go:46 +// walkZip ← createZip internal/find/zip.go:46 +// [impact] +// risk MEDIUM +// domains CLIInfrastructure · SupermodelAPI +// direct 2 +// transitive 3 +// affects cmd/find.go · internal/find/integration_test.go diff --git a/internal/find/zip_test.graph.go b/internal/find/zip_test.graph.go new file mode 100644 index 0000000..2a259e5 --- /dev/null +++ b/internal/find/zip_test.graph.go @@ -0,0 +1,12 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [calls] +// TestWalkZip_IncludesFiles → readZipEntries internal/find/zip_test.go:92 +// TestWalkZip_SkipsHiddenFiles → readZipEntries internal/find/zip_test.go:92 +// TestWalkZip_SkipsSkipDirs → readZipEntries internal/find/zip_test.go:92 +// readZipEntries ← TestWalkZip_IncludesFiles internal/find/zip_test.go:17 +// readZipEntries ← TestWalkZip_SkipsHiddenFiles internal/find/zip_test.go:33 +// readZipEntries ← TestWalkZip_SkipsSkipDirs internal/find/zip_test.go:52 diff --git a/internal/focus/doc.graph.go b/internal/focus/doc.graph.go new file mode 100644 index 0000000..0e7783b --- /dev/null +++ b/internal/focus/doc.graph.go @@ -0,0 +1,13 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imported-by cmd/focus.go +// [impact] +// risk MEDIUM +// domains CLIInfrastructure · SupermodelAPI +// direct 1 +// transitive 2 +// affects cmd/focus.go diff --git a/internal/focus/handler.go b/internal/focus/handler.go index e62af9f..7926e9d 100644 --- a/internal/focus/handler.go +++ b/internal/focus/handler.go @@ -8,8 +8,8 @@ import ( "sort" "strings" + "github.com/supermodeltools/cli/internal/analyze" "github.com/supermodeltools/cli/internal/api" - "github.com/supermodeltools/cli/internal/cache" "github.com/supermodeltools/cli/internal/config" "github.com/supermodeltools/cli/internal/ui" ) @@ -99,11 +99,11 @@ func extract(g *api.Graph, target string, depth int, includeTypes bool) *Slice { rels := g.Rels() - // 1. Direct imports (IMPORTS edges from file node, up to depth hops). + // 1. Direct imports (imports edges from file node, up to depth hops). sl.Imports = reachableImports(g, fileNode.ID, nodeByID, rels, depth) // 2. Functions defined in this file. - fnIDs := functionNodesForFile(g, fileNode.ID, rels) + fnIDs := functionNodesForFile(fileNode.ID, rels) calleesOf := buildCalleesOf(rels) for _, fnID := range fnIDs { @@ -124,14 +124,14 @@ func extract(g *api.Graph, target string, depth int, includeTypes bool) *Slice { return sl.Functions[i].Name < sl.Functions[j].Name }) - // 3. External callers (CALLS edges whose target is one of our functions). + // 3. External callers (calls edges whose target is one of our functions). fnIDSet := make(map[string]bool, len(fnIDs)) for _, id := range fnIDs { fnIDSet[id] = true } seenCallers := make(map[string]bool) for _, rel := range rels { - if rel.Type != "CALLS" && rel.Type != "CONTAINS_CALL" { + if rel.Type != "calls" && rel.Type != "contains_call" { continue } if !fnIDSet[rel.EndNode] { @@ -141,7 +141,7 @@ func extract(g *api.Graph, target string, depth int, includeTypes bool) *Slice { if callerNode == nil { continue } - callerFile := callerNode.Prop("file", "path") + callerFile := callerNode.Prop("filePath", "file", "path") if pathMatches(callerFile, target) { continue // skip self-calls } @@ -179,7 +179,7 @@ func reachableImports(g *api.Graph, seedID string, nodeByID map[string]*api.Node next := make([]string, 0) for _, cur := range queue { for _, rel := range rels { - if rel.Type != "IMPORTS" && rel.Type != "WILDCARD_IMPORTS" { + if rel.Type != "imports" && rel.Type != "wildcard_imports" { continue } if rel.StartNode != cur || visited[rel.EndNode] { @@ -202,22 +202,14 @@ func reachableImports(g *api.Graph, seedID string, nodeByID map[string]*api.Node } // functionNodesForFile returns the IDs of Function nodes associated with fileID -// via DEFINES_FUNCTION or DEFINES relationships. -func functionNodesForFile(g *api.Graph, fileID string, rels []api.Relationship) []string { +// via defines_function or defines relationships. +func functionNodesForFile(fileID string, rels []api.Relationship) []string { var ids []string for _, rel := range rels { if (rel.Type == "defines_function" || rel.Type == "defines") && rel.StartNode == fileID { ids = append(ids, rel.EndNode) } } - // Fallback: match by file property on Function nodes. - if len(ids) == 0 { - for _, n := range g.NodesByLabel("Function") { - if n.Prop("file", "path") != "" { - ids = append(ids, n.ID) - } - } - } return ids } @@ -225,7 +217,8 @@ func functionNodesForFile(g *api.Graph, fileID string, rels []api.Relationship) func buildCalleesOf(rels []api.Relationship) map[string][]string { m := make(map[string][]string) for _, rel := range rels { - if rel.Type == "calls" || rel.Type == "contains_call" { + t := rel.Type + if t == "calls" || t == "contains_call" { m[rel.StartNode] = append(m[rel.StartNode], rel.EndNode) } } @@ -235,7 +228,7 @@ func buildCalleesOf(rels []api.Relationship) map[string][]string { func extractTypes(g *api.Graph, fileID string, nodeByID map[string]*api.Node, rels []api.Relationship) []Type { var types []Type for _, rel := range rels { - if rel.Type != "DECLARES_CLASS" && rel.Type != "DEFINES" { + if rel.Type != "declares_class" && rel.Type != "defines" { continue } if rel.StartNode != fileID { @@ -337,32 +330,6 @@ func printMarkdown(w io.Writer, sl *Slice) error { return nil } -// --- Graph retrieval (not importing analyze slice) --------------------------- - func getGraph(ctx context.Context, cfg *config.Config, dir string, force bool) (*api.Graph, string, error) { - zipPath, err := createZip(dir) - if err != nil { - return nil, "", err - } - defer os.Remove(zipPath) - - hash, err := cache.HashFile(zipPath) - if err != nil { - return nil, "", err - } - if !force { - if g, _ := cache.Get(hash); g != nil { - return g, hash, nil - } - } - spin := ui.Start("Analyzing repository…") - defer spin.Stop() - - client := newAPIClient(cfg) - g, err := client.Analyze(ctx, zipPath, "focus-"+hash[:16]) - if err != nil { - return nil, hash, err - } - _ = cache.Put(hash, g) - return g, hash, nil + return analyze.GetGraph(ctx, cfg, dir, force) } diff --git a/internal/focus/handler.graph.go b/internal/focus/handler.graph.go new file mode 100644 index 0000000..6015565 --- /dev/null +++ b/internal/focus/handler.graph.go @@ -0,0 +1,50 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imports internal/api/client.go +// imports internal/api/doc.go +// imports internal/api/types.go +// imports internal/cache/cache.go +// imports internal/cache/doc.go +// imports internal/cache/fingerprint.go +// imports internal/config/config.go +// imports internal/config/doc.go +// imports internal/ui/doc.go +// imports internal/ui/output.go +// imported-by cmd/focus.go +// [calls] +// Run ← init cmd/focus.go:10 +// Run → getGraph internal/focus/handler.go:342 +// Run → extract internal/focus/handler.go:72 +// Run → render internal/focus/handler.go:286 +// buildCalleesOf ← extract internal/focus/handler.go:72 +// estimateTokens ← extract internal/focus/handler.go:72 +// extract ← Run internal/focus/handler.go:55 +// extract → pathMatches internal/focus/handler.go:278 +// extract → reachableImports internal/focus/handler.go:173 +// extract → functionNodesForFile internal/focus/handler.go:206 +// extract → buildCalleesOf internal/focus/handler.go:225 +// extract → extractTypes internal/focus/handler.go:235 +// extract → estimateTokens internal/focus/handler.go:264 +// extractTypes ← extract internal/focus/handler.go:72 +// functionNodesForFile ← extract internal/focus/handler.go:72 +// getGraph ← Run internal/focus/handler.go:55 +// getGraph → HashFile internal/cache/cache.go:59 +// getGraph → Get internal/cache/cache.go:28 +// getGraph → Start internal/ui/output.go:74 +// getGraph → Put internal/cache/cache.go:44 +// pathMatches ← extract internal/focus/handler.go:72 +// printMarkdown ← render internal/focus/handler.go:286 +// reachableImports ← extract internal/focus/handler.go:72 +// render ← Run internal/focus/handler.go:55 +// render → printMarkdown internal/focus/handler.go:293 +// render → JSON internal/ui/output.go:47 +// [impact] +// risk MEDIUM +// domains CLIInfrastructure · SupermodelAPI +// direct 1 +// transitive 2 +// affects cmd/focus.go diff --git a/internal/focus/zip.go b/internal/focus/zip.go deleted file mode 100644 index 83ed7d9..0000000 --- a/internal/focus/zip.go +++ /dev/null @@ -1,129 +0,0 @@ -package focus - -import ( - "archive/zip" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/supermodeltools/cli/internal/api" - "github.com/supermodeltools/cli/internal/config" -) - -// skipDirs are directory names that should never be included in the archive. -var skipDirs = map[string]bool{ - ".git": true, - ".claude": true, - ".idea": true, - ".vscode": true, - ".cache": true, - ".turbo": true, - ".nx": true, - ".next": true, - ".nuxt": true, - ".terraform": true, - ".tox": true, - ".venv": true, - ".pnpm-store": true, - "__pycache__": true, - "__snapshots__": true, - "bower_components": true, - "build": true, - "coverage": true, - "dist": true, - "node_modules": true, - "out": true, - "target": true, - "vendor": true, - "venv": true, -} - -// createZip archives the repository at dir into a temporary ZIP file and -// returns its path. The caller is responsible for removing the file. -// -// Strategy: use git archive when inside a Git repo (respects .gitignore, -// deterministic output). Falls back to a manual directory walk otherwise. -func createZip(dir string) (string, error) { - f, err := os.CreateTemp("", "supermodel-*.zip") - if err != nil { - return "", fmt.Errorf("create temp file: %w", err) - } - dest := f.Name() - f.Close() - - if isGitRepo(dir) { - if err := gitArchive(dir, dest); err == nil { - return dest, nil - } - } - - if err := walkZip(dir, dest); err != nil { - os.Remove(dest) - return "", err - } - return dest, nil -} - -func isGitRepo(dir string) bool { - cmd := exec.Command("git", "-C", dir, "rev-parse", "--git-dir") - cmd.Stdout = io.Discard - cmd.Stderr = io.Discard - return cmd.Run() == nil -} - -func gitArchive(dir, dest string) error { - cmd := exec.Command("git", "-C", dir, "archive", "--format=zip", "-o", dest, "HEAD") - cmd.Stderr = os.Stderr - return cmd.Run() -} - -// walkZip creates a ZIP of dir, excluding skipDirs, hidden files, and -// files larger than 10 MB. -func walkZip(dir, dest string) error { - out, err := os.Create(dest) - if err != nil { - return err - } - defer out.Close() - - zw := zip.NewWriter(out) - defer zw.Close() - - return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - rel, err := filepath.Rel(dir, path) - if err != nil { - return err - } - if info.IsDir() { - if skipDirs[info.Name()] { - return filepath.SkipDir - } - return nil - } - if strings.HasPrefix(info.Name(), ".") || info.Size() > 10<<20 { - return nil - } - w, err := zw.Create(filepath.ToSlash(rel)) - if err != nil { - return err - } - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(w, f) - return err - }) -} - -// newAPIClient wraps api.New for use within the focus slice. -func newAPIClient(cfg *config.Config) *api.Client { - return api.New(cfg) -} diff --git a/internal/focus/zip.graph.go b/internal/focus/zip.graph.go new file mode 100644 index 0000000..71a727e --- /dev/null +++ b/internal/focus/zip.graph.go @@ -0,0 +1,25 @@ +//go:build ignore + +package ignore + +// @generated supermodel-sidecar — do not edit +// [deps] +// imports internal/api/client.go +// imports internal/api/doc.go +// imports internal/api/types.go +// imports internal/config/config.go +// imports internal/config/doc.go +// imported-by cmd/focus.go +// [calls] +// createZip → isGitRepo internal/focus/zip.go:70 +// createZip → gitArchive internal/focus/zip.go:77 +// createZip → walkZip internal/focus/zip.go:85 +// gitArchive ← createZip internal/focus/zip.go:49 +// isGitRepo ← createZip internal/focus/zip.go:49 +// walkZip ← createZip internal/focus/zip.go:49 +// [impact] +// risk MEDIUM +// domains CLIInfrastructure · SupermodelAPI +// direct 1 +// transitive 2 +// affects cmd/focus.go