diff --git a/code/semantic_index.go b/code/semantic_index.go index 121d9cfb..aff8eb84 100644 --- a/code/semantic_index.go +++ b/code/semantic_index.go @@ -30,7 +30,7 @@ import ( tstsx "github.com/smacker/go-tree-sitter/typescript/tsx" ) -const semanticAnalyzerVersion = "codefly.semantic-index/v2" +const semanticAnalyzerVersion = "codefly.semantic-index/v3" type semanticLanguage struct { name string diff --git a/code/semantic_index_test.go b/code/semantic_index_test.go index 04ba996b..7359f03e 100644 --- a/code/semantic_index_test.go +++ b/code/semantic_index_test.go @@ -139,6 +139,43 @@ func TestSemanticIndexReportsDegradedAndNotAttemptedCoverage(t *testing.T) { }) } +func TestSemanticIndexTraversesLocalDirectorySymlinkRoot(t *testing.T) { + project := t.TempDir() + if err := os.WriteFile(filepath.Join(project, "app.py"), []byte("def run():\n return 1\n"), 0o644); err != nil { + t.Fatal(err) + } + logicalRoot := filepath.Join(t.TempDir(), "code") + if err := os.Symlink(project, logicalRoot); err != nil { + t.Fatal(err) + } + physicalRoot, err := filepath.EvalSymlinks(logicalRoot) + if err != nil { + t.Fatal(err) + } + + server := NewDefaultCodeServer(logicalRoot) + if server.SourceDir != physicalRoot { + t.Fatalf("source root = %q, want physical root %q", server.SourceDir, physicalRoot) + } + response, err := server.Execute(t.Context(), &codev0.CodeRequest{Operation: &codev0.CodeRequest_GetSemanticIndex{GetSemanticIndex: &codev0.GetSemanticIndexRequest{}}}) + if err != nil { + t.Fatal(err) + } + index := response.GetGetSemanticIndex() + if index.GetState() != basev0.SemanticIndexState_SEMANTIC_INDEX_STATE_COMPLETE || len(index.GetFiles()) != 1 || index.GetFiles()[0].GetPath() != "app.py" { + t.Fatalf("semantic index = %#v, want one root-relative Python file", index) + } + if index.GetAnalyzerVersion() != "codefly.semantic-index/v3" { + t.Fatalf("analyzer version = %q, want the symlink-root contract generation", index.GetAnalyzerVersion()) + } + + customRoot := filepath.Join(logicalRoot, "opaque") + custom := NewDefaultCodeServer(customRoot, WithVFS(NewMemoryVFS())) + if custom.SourceDir != customRoot { + t.Fatalf("custom VFS root = %q, want opaque identity %q", custom.SourceDir, customRoot) + } +} + func TestSemanticIndexExcludesLocalDataAndQualifiesNestedCallables(t *testing.T) { root := t.TempDir() body := `package main diff --git a/code/server.go b/code/server.go index 1dc14a43..a2de71a0 100644 --- a/code/server.go +++ b/code/server.go @@ -120,6 +120,18 @@ func NewDefaultCodeServer(sourceDir string, opts ...ServerOption) *DefaultCodeSe o(s) } } + // ARCHITECTURE: arbitrary-source Codefly services expose the project as a + // directory symlink inside an ephemeral service. filepath.WalkDir does not + // traverse a directory symlink used as its root, so retaining that logical + // path makes every scan look like an empty project. Canonicalize only the + // production LocalVFS boundary; custom VFS roots are opaque identities and + // must never be interpreted through the host filesystem. + switch s.FS.(type) { + case LocalVFS, *LocalVFS: + if resolved, err := filepath.EvalSymlinks(s.SourceDir); err == nil { + s.SourceDir = resolved + } + } // Apply CachedVFS after all options (needs final SourceDir) if s.wantCachedFS { if cached, err := NewCachedVFS(s.FS, s.SourceDir); err == nil {