Skip to content

Commit 3bffc20

Browse files
tae2089claude
andcommitted
feat: add PruneTree helper and depth parameter to get_rag_tree
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f3fab5a commit 3bffc20

5 files changed

Lines changed: 157 additions & 1 deletion

File tree

internal/mcp/handlers.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1383,8 +1383,9 @@ func (h *handlers) buildRagIndex(ctx context.Context, request mcp.CallToolReques
13831383

13841384
func (h *handlers) getRagTree(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
13851385
communityID := request.GetString("community_id", "")
1386+
depth := int(request.GetFloat("depth", 0))
13861387

1387-
key := "get_rag_tree:" + mustJSON(map[string]any{"community_id": communityID})
1388+
key := "get_rag_tree:" + mustJSON(map[string]any{"community_id": communityID, "depth": depth})
13881389
if h.cache != nil {
13891390
if cached, ok := h.cache.Get(key); ok {
13901391
return mcp.NewToolResultText(cached), nil
@@ -1406,6 +1407,10 @@ func (h *handlers) getRagTree(ctx context.Context, request mcp.CallToolRequest)
14061407
}
14071408
}
14081409

1410+
if depth > 0 {
1411+
node = ragindex.PruneTree(node, depth)
1412+
}
1413+
14091414
b, err := json.Marshal(node)
14101415
if err != nil {
14111416
return mcp.NewToolResultError(fmt.Sprintf("marshal tree: %v", err)), nil

internal/mcp/handlers_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/imtaebin/code-context-graph/internal/analysis/incremental"
2828
"github.com/imtaebin/code-context-graph/internal/analysis/query"
2929
"github.com/imtaebin/code-context-graph/internal/model"
30+
"github.com/imtaebin/code-context-graph/internal/ragindex"
3031
"github.com/imtaebin/code-context-graph/internal/store/gormstore"
3132
"github.com/imtaebin/code-context-graph/internal/store/search"
3233
)
@@ -2110,3 +2111,69 @@ func TestGetRagTree_InvalidCommunityID(t *testing.T) {
21102111
t.Fatal("expected error for nonexistent community_id")
21112112
}
21122113
}
2114+
2115+
func TestGetRagTree_DepthLimitsChildren(t *testing.T) {
2116+
deps := setupTestDeps(t)
2117+
2118+
// 임시 인덱스 디렉토리 설정
2119+
tmpDir := t.TempDir()
2120+
deps.RagIndexDir = filepath.Join(tmpDir, ".ccg")
2121+
2122+
// DB에 community + node + CommunityMembership 생성
2123+
community := model.Community{Key: "auth", Label: "Auth Community", Strategy: "auto"}
2124+
if err := deps.DB.Create(&community).Error; err != nil {
2125+
t.Fatalf("create community: %v", err)
2126+
}
2127+
2128+
node := model.Node{
2129+
QualifiedName: "auth.Login",
2130+
Kind: model.NodeKindFunction,
2131+
Name: "Login",
2132+
FilePath: "internal/auth/login.go",
2133+
StartLine: 1,
2134+
EndLine: 10,
2135+
Language: "go",
2136+
}
2137+
if err := deps.DB.Create(&node).Error; err != nil {
2138+
t.Fatalf("create node: %v", err)
2139+
}
2140+
2141+
membership := model.CommunityMembership{
2142+
CommunityID: community.ID,
2143+
NodeID: node.ID,
2144+
}
2145+
if err := deps.DB.Create(&membership).Error; err != nil {
2146+
t.Fatalf("create membership: %v", err)
2147+
}
2148+
2149+
// ragindex.Builder로 인덱스 빌드
2150+
b := &ragindex.Builder{
2151+
DB: deps.DB,
2152+
OutDir: filepath.Join(tmpDir, "docs"),
2153+
IndexDir: deps.RagIndexDir,
2154+
}
2155+
if _, _, err := b.Build(); err != nil {
2156+
t.Fatalf("Build: %v", err)
2157+
}
2158+
2159+
// depth=1로 get_rag_tree 호출: community 노드는 있지만 파일 노드는 없어야 함
2160+
result := callTool(t, deps, "get_rag_tree", map[string]any{
2161+
"depth": float64(1),
2162+
})
2163+
if result.IsError {
2164+
t.Fatalf("get_rag_tree error: %v", getTextContent(result))
2165+
}
2166+
2167+
var treeNode ragindex.TreeNode
2168+
if err := json.Unmarshal([]byte(getTextContent(result)), &treeNode); err != nil {
2169+
t.Fatalf("unmarshal tree: %v", err)
2170+
}
2171+
2172+
if len(treeNode.Children) == 0 {
2173+
t.Fatal("expected community nodes at depth=1, got none")
2174+
}
2175+
communityNode := treeNode.Children[0]
2176+
if len(communityNode.Children) != 0 {
2177+
t.Fatalf("expected 0 file children at depth=1, got %d", len(communityNode.Children))
2178+
}
2179+
}

internal/mcp/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ func NewServer(deps *Deps) *server.MCPServer {
266266
Tool: mcp.NewTool("get_rag_tree",
267267
mcp.WithDescription("Get the RAG document tree for navigation. Call without arguments first to see all communities, then pass community_id to drill into a specific one."),
268268
mcp.WithString("community_id", mcp.Description("Community node ID as shown in the tree (e.g. 'community:auth'). Omit to get the full tree.")),
269+
mcp.WithNumber("depth", mcp.Description("Maximum tree depth to return (1=communities only, 2=communities+files, 3=including symbols). Default: 0 (unlimited).")),
269270
),
270271
Handler: h.getRagTree,
271272
},

internal/ragindex/builder.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,31 @@ func FindNode(root *TreeNode, id string) *TreeNode {
303303
}
304304
return nil
305305
}
306+
307+
// PruneTree는 root 트리를 maxDepth 깊이까지만 포함한 새 트리를 반환한다.
308+
// maxDepth <= 0이면 전체 트리를 반환한다. 원본 트리는 변경하지 않는다.
309+
// depth 계산: root는 depth 0, root의 직계 자식은 depth 1.
310+
func PruneTree(root *TreeNode, maxDepth int) *TreeNode {
311+
if root == nil {
312+
return nil
313+
}
314+
return pruneNode(root, 0, maxDepth)
315+
}
316+
317+
func pruneNode(n *TreeNode, currentDepth, maxDepth int) *TreeNode {
318+
copied := &TreeNode{
319+
ID: n.ID,
320+
Label: n.Label,
321+
Summary: n.Summary,
322+
DocPath: n.DocPath,
323+
}
324+
if maxDepth <= 0 || currentDepth < maxDepth {
325+
copied.Children = make([]*TreeNode, 0, len(n.Children))
326+
for _, child := range n.Children {
327+
copied.Children = append(copied.Children, pruneNode(child, currentDepth+1, maxDepth))
328+
}
329+
} else {
330+
copied.Children = []*TreeNode{}
331+
}
332+
return copied
333+
}

internal/ragindex/builder_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,61 @@ func TestBuilder_ProjectDesc(t *testing.T) {
328328
}
329329
}
330330

331+
// TestPruneTree_Depth1: depth=1이면 root와 직계 자식만 반환, 손자 노드 없음.
332+
func TestPruneTree_Depth1(t *testing.T) {
333+
root := &ragindex.TreeNode{
334+
ID: "root",
335+
Label: "Root",
336+
Children: []*ragindex.TreeNode{
337+
{
338+
ID: "c1",
339+
Label: "Community 1",
340+
Children: []*ragindex.TreeNode{
341+
{ID: "f1", Label: "file.go"},
342+
},
343+
},
344+
},
345+
}
346+
347+
result := ragindex.PruneTree(root, 1)
348+
if len(result.Children) != 1 {
349+
t.Fatalf("expected 1 child, got %d", len(result.Children))
350+
}
351+
if len(result.Children[0].Children) != 0 {
352+
t.Fatalf("expected 0 grandchildren at depth=1, got %d", len(result.Children[0].Children))
353+
}
354+
// 원본 트리는 변경되지 않아야 함
355+
if len(root.Children[0].Children) != 1 {
356+
t.Fatal("PruneTree must not modify the original tree")
357+
}
358+
}
359+
360+
// TestPruneTree_NegativeDepth: depth <= 0이면 트리 전체를 반환한다.
361+
func TestPruneTree_NegativeDepth(t *testing.T) {
362+
root := &ragindex.TreeNode{
363+
ID: "root",
364+
Children: []*ragindex.TreeNode{
365+
{ID: "c1", Children: []*ragindex.TreeNode{{ID: "f1"}}},
366+
},
367+
}
368+
369+
result := ragindex.PruneTree(root, 0)
370+
if len(result.Children) != 1 {
371+
t.Fatalf("expected 1 child, got %d", len(result.Children))
372+
}
373+
if len(result.Children[0].Children) != 1 {
374+
t.Fatalf("expected 1 grandchild at depth=0 (unlimited), got %d", len(result.Children[0].Children))
375+
}
376+
}
377+
378+
// TestPruneTree_NilRoot: nil 입력 → nil 반환.
379+
func TestPruneTree_NilRoot(t *testing.T) {
380+
result := ragindex.PruneTree(nil, 2)
381+
if result != nil {
382+
t.Fatal("expected nil for nil root")
383+
}
384+
}
385+
331386
// TestFindNode: FindNode가 재귀적으로 트리에서 노드를 찾는지 검증한다.
332387
func TestFindNode(t *testing.T) {
333388
root := &ragindex.TreeNode{

0 commit comments

Comments
 (0)