From f82bd486147b205114fe433cc44eecfcde7a5bc5 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Sat, 8 Aug 2026 22:12:19 -0400 Subject: [PATCH 1/2] fix(code): canonicalize local symlink source roots --- code/semantic_index_test.go | 34 ++++++++++++++++++++++++++++++++++ code/server.go | 12 ++++++++++++ 2 files changed, 46 insertions(+) diff --git a/code/semantic_index_test.go b/code/semantic_index_test.go index 04ba996b..3255792b 100644 --- a/code/semantic_index_test.go +++ b/code/semantic_index_test.go @@ -139,6 +139,40 @@ 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) + } + + 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 { From 016d021f7fb32a8e0ca4ed797ec5a054b8ac4695 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Sat, 8 Aug 2026 22:18:46 -0400 Subject: [PATCH 2/2] fix(code): advance semantic analyzer contract --- code/semantic_index.go | 2 +- code/semantic_index_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 3255792b..7359f03e 100644 --- a/code/semantic_index_test.go +++ b/code/semantic_index_test.go @@ -165,6 +165,9 @@ func TestSemanticIndexTraversesLocalDirectorySymlinkRoot(t *testing.T) { 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()))