@@ -47,6 +47,13 @@ func (b *Builder) indexDir() string {
4747 return b .IndexDir
4848}
4949
50+ // nodeInfo는 Builder 내부에서 노드의 파일 정보를 담는 구조체이다.
51+ type nodeInfo struct {
52+ FilePath string
53+ Name string
54+ QualifiedName string
55+ }
56+
5057// Build는 DB에서 커뮤니티와 멤버 노드를 읽어 doc-index.json을 생성한다.
5158// 반환값: (커뮤니티 수, 파일 수, 에러)
5259func (b * Builder ) Build () (int , int , error ) {
@@ -59,30 +66,34 @@ func (b *Builder) Build() (int, int, error) {
5966 }
6067 slog .Debug ("커뮤니티 로드 완료" , "count" , len (communities ))
6168
62- // 2. 1-pass: 모든 커뮤니티의 고유 파일 경로 수집
69+ // 2. 1-pass: 모든 커뮤니티의 고유 node ID 수집
6370 allNodeIDs := make ([]uint , 0 )
6471 for _ , comm := range communities {
6572 for _ , m := range comm .Members {
6673 allNodeIDs = append (allNodeIDs , m .NodeID )
6774 }
6875 }
6976
70- // 노드 ID → 파일 경로 매핑
71- nodeFileMap := make (map [uint ]string )
77+ // 노드 ID → nodeInfo 매핑
78+ nodeInfoMap := make (map [uint ]nodeInfo )
7279 if len (allNodeIDs ) > 0 {
7380 var nodes []model.Node
7481 if err := b .DB .Where ("id IN ?" , allNodeIDs ).Find (& nodes ).Error ; err != nil {
7582 return 0 , 0 , fmt .Errorf ("load all nodes: %w" , err )
7683 }
7784 for _ , n := range nodes {
78- nodeFileMap [n .ID ] = n .FilePath
85+ nodeInfoMap [n .ID ] = nodeInfo {
86+ FilePath : n .FilePath ,
87+ Name : n .Name ,
88+ QualifiedName : n .QualifiedName ,
89+ }
7990 }
8091 }
8192
8293 // 고유 파일 경로 목록 수집
8394 filePathSet := make (map [string ]struct {})
84- for _ , fp := range nodeFileMap {
85- filePathSet [fp ] = struct {}{}
95+ for _ , info := range nodeInfoMap {
96+ filePathSet [info . FilePath ] = struct {}{}
8697 }
8798 allFilePaths := make ([]string , 0 , len (filePathSet ))
8899 for fp := range filePathSet {
@@ -95,6 +106,12 @@ func (b *Builder) Build() (int, int, error) {
95106 return 0 , 0 , fmt .Errorf ("batchFileSummaries: %w" , err )
96107 }
97108
109+ // 4. @intent 태그를 가진 symbol 노드 배치 조회
110+ symbolsByFile , err := b .batchSymbolNodes (allNodeIDs , nodeInfoMap )
111+ if err != nil {
112+ return 0 , 0 , fmt .Errorf ("batchSymbolNodes: %w" , err )
113+ }
114+
98115 root := & TreeNode {
99116 ID : "root" ,
100117 Label : "Root" ,
@@ -104,7 +121,7 @@ func (b *Builder) Build() (int, int, error) {
104121
105122 uniqueFiles := make (map [string ]struct {})
106123
107- // 4 . 2-pass: 커뮤니티별 TreeNode 구성
124+ // 5 . 2-pass: 커뮤니티별 TreeNode 구성
108125 for _ , comm := range communities {
109126 slog .Debug ("커뮤니티 처리 중" , "key" , comm .Key , "members" , len (comm .Members ))
110127
@@ -119,8 +136,8 @@ func (b *Builder) Build() (int, int, error) {
119136 // 이 커뮤니티의 고유 파일 경로 수집
120137 commFilePathSet := make (map [string ]struct {})
121138 for _ , m := range comm .Members {
122- if fp , ok := nodeFileMap [m .NodeID ]; ok {
123- commFilePathSet [fp ] = struct {}{}
139+ if info , ok := nodeInfoMap [m .NodeID ]; ok {
140+ commFilePathSet [info . FilePath ] = struct {}{}
124141 }
125142 }
126143 slog .Debug ("파일 경로 그룹 완료" , "community" , comm .Key , "files" , len (commFilePathSet ))
@@ -134,7 +151,7 @@ func (b *Builder) Build() (int, int, error) {
134151 Label : filepath .Base (filePath ),
135152 Summary : summary ,
136153 DocPath : docPath ,
137- Children : [] * TreeNode {} ,
154+ Children : symbolsByFile [ filePath ] ,
138155 }
139156 uniqueFiles [filePath ] = struct {}{}
140157 commNode .Children = append (commNode .Children , fileNode )
@@ -144,14 +161,14 @@ func (b *Builder) Build() (int, int, error) {
144161 root .Children = append (root .Children , commNode )
145162 }
146163
147- // 5 . Index 구조체 구성
164+ // 6 . Index 구조체 구성
148165 idx := & Index {
149166 Version : 1 ,
150167 BuiltAt : time .Now ().UTC (),
151168 Root : root ,
152169 }
153170
154- // 6 . doc-index.json 파일 기록 (원자적 쓰기)
171+ // 7 . doc-index.json 파일 기록 (원자적 쓰기)
155172 if err := b .writeIndex (idx ); err != nil {
156173 return 0 , 0 , fmt .Errorf ("writeIndex: %w" , err )
157174 }
@@ -219,6 +236,53 @@ func (b *Builder) batchFileSummaries(filePaths []string) (map[string]string, err
219236}
220237
221238
239+ // batchSymbolNodes는 @intent 태그를 가진 노드를 filePath → []*TreeNode 맵으로 반환한다.
240+ // 노드당 첫 번째 @intent 값만 summary로 사용한다.
241+ func (b * Builder ) batchSymbolNodes (nodeIDs []uint , infoMap map [uint ]nodeInfo ) (map [string ][]* TreeNode , error ) {
242+ result := make (map [string ][]* TreeNode )
243+ if len (nodeIDs ) == 0 {
244+ return result , nil
245+ }
246+
247+ type intentRow struct {
248+ NodeID uint
249+ QualifiedName string
250+ Name string
251+ FilePath string
252+ Value string
253+ }
254+
255+ var rows []intentRow
256+ if err := b .DB .Table ("nodes" ).
257+ Select ("nodes.id as node_id, nodes.qualified_name, nodes.name, nodes.file_path, doc_tags.value" ).
258+ Joins ("JOIN annotations ON annotations.node_id = nodes.id" ).
259+ Joins ("JOIN doc_tags ON doc_tags.annotation_id = annotations.id" ).
260+ Where ("nodes.id IN ? AND doc_tags.kind = ?" , nodeIDs , string (model .TagIntent )).
261+ Order ("nodes.file_path ASC, doc_tags.ordinal ASC, doc_tags.id ASC" ).
262+ Scan (& rows ).Error ; err != nil {
263+ return nil , fmt .Errorf ("batch symbol nodes: %w" , err )
264+ }
265+
266+ // 첫 번째 @intent 태그만 사용 (node_id 기준 deduplicate)
267+ seen := make (map [uint ]struct {})
268+ for _ , r := range rows {
269+ if _ , ok := seen [r .NodeID ]; ok {
270+ continue
271+ }
272+ seen [r .NodeID ] = struct {}{}
273+
274+ symNode := & TreeNode {
275+ ID : fmt .Sprintf ("symbol:%s" , r .QualifiedName ),
276+ Label : r .Name ,
277+ Summary : r .Value ,
278+ Children : []* TreeNode {},
279+ }
280+ result [r .FilePath ] = append (result [r .FilePath ], symNode )
281+ }
282+
283+ return result , nil
284+ }
285+
222286// docPath는 파일 경로를 기반으로 docs 디렉토리 내의 문서 경로를 반환한다.
223287// 전체 상대 경로 구조를 유지하여 basename 충돌을 방지한다.
224288// 예: "internal/mcp/handlers.go" → "docs/internal/mcp/handlers.go.md"
0 commit comments