diff --git a/AGENTS.md b/AGENTS.md index 43e9701..8c27f9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ Use `.ccg.yaml` to manage project defaults such as exclude patterns and DB setti When looking for code locations, related implementations, call relationships, impact radius, or architecture context, use ccg MCP tools and Agent Skills first. -- For natural-language code understanding, module exploration, and architecture context, prefer the `/ccg-docs` skill and `retrieve_docs`, `get_rag_tree`, `get_doc_content`. +- For natural-language code understanding, module exploration, and architecture context, prefer the `/ccg-docs` skill: use `search_docs` to find relevant docs, then `get_doc_content` to read one (with `get_rag_tree` for module context). - For exact symbol locations, call relationships, and graph metadata, use ccg MCP `query_graph`, `get_node`, `get_minimal_context`, or the `/ccg` skill. - For annotation/keyword-based candidate search, use ccg MCP `search` or `ccg search` as a supplement. - For impact radius, flows, dead code, and structural analysis, prefer the `/ccg-analyze` skill and related MCP tools (`get_impact_radius`, `trace_flow`, `find_dead_code`, `get_architecture_overview`). diff --git a/CLAUDE.md b/CLAUDE.md index f780d11..2a4026a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,6 @@ Follow the global prompt rules first. This file adds project-specific skill routing for a project that uses the `agent-team` CLI (github.com/tae2089/agent-team) as its durable work ledger. ## MCP 서버 - `.mcp.json`에 등록된 ccg MCP 서버가 21개 도구를 제공합니다: - `parse_project`, `build_or_update_graph`, `run_postprocess` diff --git a/README.md b/README.md index ae99f98..12d0b1c 100644 --- a/README.md +++ b/README.md @@ -94,18 +94,18 @@ Wiki compatibility snapshot. The Wiki prefers the graph database for tree navigation and search, then uses `wiki-index.json` only when DB-backed navigation is unavailable. By default `ccg docs` also refreshes community structure and writes `.ccg/doc-index.json` as a compatibility snapshot for -manual RAG-index workflows; runtime `retrieve_docs` uses DB-backed graph and +manual RAG-index workflows; runtime `search_docs` uses DB-backed graph and annotation evidence. Use `--rag=false` when you only want Markdown and the Wiki snapshot, or `--rag-refresh=false` when you want to rebuild the RAG index from existing community rows without recalculating communities. -For LLM agents, use DB-backed `retrieve_docs` as the first stop for broad +For LLM agents, use DB-backed `search_docs` as the first stop for broad natural-language questions such as "how does webhook sync work?" or "where are the operational risks?". It is not a Top1 search engine; it is an evidence-driven narrowing layer that should return a small set of relevant -files with `matched_fields`, `matched_terms`, and evidence nodes. Use -`get_doc_content`, `get_node`, `query_graph`, `trace_flow`, and impact tools -only after the route is narrowed. Use `ccg search` as a focused +files with `matched_fields`, `matched_terms`, and evidence nodes. Read the +narrowed docs with `get_doc_content`, then use `get_node`, `query_graph`, +`trace_flow`, and impact tools only after the route is narrowed. Use `ccg search` as a focused annotation/keyword candidate search rather than the first tool for broad code understanding. @@ -125,7 +125,7 @@ size stays small. The Wiki is meant for developers and agents inspecting a generated codebase: - Tree navigation over folders, packages, files, and annotated symbols -- Keyword search and DB-backed `retrieve_docs` with matched evidence and small +- Keyword search and DB-backed `search_docs` with matched evidence and small file-level result sets - Rich symbol detail cards from CCG annotations even when a symbol has no generated Markdown file diff --git a/guide/README.md b/guide/README.md index d402392..af99ed9 100644 --- a/guide/README.md +++ b/guide/README.md @@ -2,10 +2,11 @@ Documentation index for code-context-graph. -For LLM-agent workflows, start natural-language code exploration from -DB-backed `retrieve_docs`. It is an evidence-driven narrowing layer, not a Top1 -search engine: use its small file-level candidates, matched fields, and evidence -nodes to choose the shortest route into docs or graph tools. +For LLM-agent workflows, start natural-language code exploration from the +docs/RAG path: use `search_docs` to find relevant docs, then `get_doc_content` +to read one. Treat these as an evidence-driven narrowing layer, not a Top1 +search engine: use the small file-level candidates to choose the shortest route +into docs or graph tools. The browser Wiki is served by `ccg-server` when `--wiki-dir` points at built React assets. It prefers the graph database for presentation, uses diff --git a/guide/annotations.md b/guide/annotations.md index f48cc72..bb42d68 100644 --- a/guide/annotations.md +++ b/guide/annotations.md @@ -6,9 +6,9 @@ focused search can leverage business context. Annotations are indexed for index. For LLM-agent natural-language exploration, prefer the docs/RAG path first: -`ccg docs`, then MCP `retrieve_docs`, `get_rag_tree`, and `get_doc_content`. -Use `ccg search` when you need a focused list of -annotation/keyword-matched symbol candidates. +`ccg docs`, then use MCP `search_docs` to find relevant docs and `get_doc_content` +to read one (with `get_rag_tree` for module context). Use `ccg search` when you +need a focused list of annotation/keyword-matched symbol candidates. Annotation quality is validated by `ccg lint`. For category meanings such as `unannotated`, `incomplete`, `dead-ref`, `contradiction`, and `drifted`, see [Lint Guide](lint.md). @@ -65,7 +65,7 @@ The path and symbol are optional, so `ccg://auth-svc/internal/auth` can point at ## Retrieval Quality -Annotations are retrieval features. `retrieve_docs` and generated docs rank +Annotations are retrieval features. `search_docs` and generated docs rank file-level evidence from structured buckets such as `@index`, `@intent`, `@domainRule`, `@sideEffect`, `@requires`, `@ensures`, and `@see`. Better annotations make natural-language retrieval more precise, but only when the diff --git a/guide/cli-reference.md b/guide/cli-reference.md index 110bcae..d6bc555 100644 --- a/guide/cli-reference.md +++ b/guide/cli-reference.md @@ -72,7 +72,7 @@ CCG has two search surfaces with different jobs: | Use case | Preferred entrypoint | |----------|----------------------| -| Natural-language code understanding, module exploration, architecture questions | `ccg docs`, then MCP `retrieve_docs`, `get_rag_tree`, `get_doc_content` | +| Natural-language code understanding, module exploration, architecture questions | `ccg docs`, then MCP `search_docs`, `get_rag_tree`, `get_doc_content` | | Exact symbol lookup, callers/callees, imports, bounded graph traversal | MCP `get_node`, `query_graph`, `get_minimal_context` | | Impact analysis, flow tracing | MCP analysis tools such as `get_impact_radius`, `trace_flow` | | Focused annotation/keyword candidate search | `ccg search` or MCP `search` | @@ -104,12 +104,11 @@ Use `--rag-refresh=false` only when you intentionally want to reuse existing community rows. The standalone `ccg rag-index` command remains available for manual rebuilds from generated docs and already-computed communities. -Then use MCP `retrieve_docs` to retrieve file-level candidates and bounded -Markdown content with matched fields and graph evidence. Use `get_rag_tree` to expand the -module/community context and `get_doc_content` to read a specific generated doc -directly. `search_docs` and `ccg search` remain useful for quick keyword or -annotation matches, but they should not be treated as the primary answering -surface for broad natural-language questions. +Then use MCP `search_docs` to find relevant docs and `get_doc_content` to read +one directly. Use `get_rag_tree` to expand the module/community context before +drilling into a specific generated doc. `ccg search` remains useful for quick +keyword or annotation matches over symbols, but it should not be treated as the +primary answering surface for broad natural-language questions. ### Database Choice diff --git a/guide/ko/README.md b/guide/ko/README.md index 8239ef4..9d5cb8c 100644 --- a/guide/ko/README.md +++ b/guide/ko/README.md @@ -6,10 +6,10 @@ code-context-graph의 문서 인덱스입니다. 한국어 문서는 영어 가 CCG는 GPT, Claude, Codex 같은 LLM 기반 코딩 에이전트가 개발 중 코드베이스 맥락을 정확하고 작게 가져오도록 만든 로컬/셀프호스트 context infrastructure입니다. 일반 SaaS 관리자용 제품이 아니라, CLI, MCP, 로그, 문서를 이해하는 개발자와 코딩 에이전트가 코드 검색, 영향 분석, 호출 그래프, 문서/RAG, bounded response를 활용하도록 설계되었습니다. -LLM 에이전트 워크플로우에서는 자연어 기반 코드 탐색을 DB-backed -`retrieve_docs`에서 시작하십시오. Top1 정답을 강제하기보다 작은 파일 후보, -matched fields, evidence node로 빠르게 경로를 좁힌 뒤 graph/search 도구로 -정확한 위치와 관계를 확인하는 흐름을 권장합니다. +LLM 에이전트 워크플로우에서는 자연어 기반 코드 탐색을 docs/RAG 경로에서 +시작하십시오. `search_docs`로 관련 문서를 찾은 뒤 `get_doc_content`로 하나를 +읽습니다. Top1 정답을 강제하기보다 작은 파일 후보로 빠르게 경로를 좁힌 뒤 +graph/search 도구로 정확한 위치와 관계를 확인하는 흐름을 권장합니다. 브라우저 Wiki는 `ccg-server`에서 `--wiki-dir`가 빌드된 React asset을 가리킬 때 제공됩니다. 표시는 graph database를 우선 사용하고, diff --git a/guide/ko/annotations.md b/guide/ko/annotations.md index ae4ff33..497d9f4 100644 --- a/guide/ko/annotations.md +++ b/guide/ko/annotations.md @@ -4,7 +4,7 @@ 코드에 구조화된 메타데이터를 추가하여 AI, 생성 문서, RAG, 집중 검색이 비즈니스 컨텍스트를 활용할 수 있도록 합니다. 어노테이션은 `ccg search`를 위해 인덱싱되며, RAG 인덱스의 입력이 되는 생성 Markdown에도 반영됩니다. -LLM 에이전트의 자연어 기반 코드 탐색에는 docs/RAG 경로를 먼저 사용하십시오. `ccg docs`를 실행한 뒤 MCP `retrieve_docs`, `get_rag_tree`, `get_doc_content`를 사용합니다. `ccg search`는 어노테이션/키워드에 매칭되는 심볼 후보 목록이 필요할 때 사용하십시오. +LLM 에이전트의 자연어 기반 코드 탐색에는 docs/RAG 경로를 먼저 사용하십시오. `ccg docs`를 실행한 뒤 MCP `search_docs`로 관련 문서를 찾고 `get_doc_content`로 하나를 읽습니다(모듈 맥락은 `get_rag_tree`). `ccg search`는 어노테이션/키워드에 매칭되는 심볼 후보 목록이 필요할 때 사용하십시오. 어노테이션 품질은 `ccg lint`에 의해 검증됩니다. `unannotated`, `incomplete`, `dead-ref`, `contradiction`, `drifted`와 같은 카테고리의 의미는 [Lint 가이드](lint.md)를 참조하십시오. @@ -61,7 +61,7 @@ path와 symbol은 선택 사항입니다. `ccg://auth-svc/internal/auth`는 패 ## Retrieval 품질 (Retrieval Quality) -어노테이션은 retrieval feature입니다. `retrieve_docs`와 생성 문서는 +어노테이션은 retrieval feature입니다. `search_docs`와 생성 문서는 `@index`, `@intent`, `@domainRule`, `@sideEffect`, `@requires`, `@ensures`, `@see` 같은 구조화된 bucket을 파일 단위 근거로 점수화합니다. 자연어 검색 품질은 좋은 어노테이션으로 올라가지만, 태그가 실제 동작을 설명할 때만 diff --git a/guide/ko/cli-reference.md b/guide/ko/cli-reference.md index 9dd14a2..25af945 100644 --- a/guide/ko/cli-reference.md +++ b/guide/ko/cli-reference.md @@ -74,7 +74,7 @@ CCG에는 역할이 다른 두 검색 표면이 있습니다. | 사용 사례 | 우선 진입점 | |----------|-------------| -| 자연어 기반 코드 이해, 모듈 탐색, 아키텍처 질문 | `ccg docs`, 이후 MCP `retrieve_docs`, `get_rag_tree`, `get_doc_content` | +| 자연어 기반 코드 이해, 모듈 탐색, 아키텍처 질문 | `ccg docs`, 이후 MCP `search_docs`, `get_rag_tree`, `get_doc_content` | | 정확한 심볼 조회, caller/callee, import, bounded graph traversal | MCP `get_node`, `query_graph`, `get_minimal_context` | | 영향 분석, flow 추적 | `get_impact_radius`, `trace_flow` 같은 MCP 분석 도구 | | 어노테이션/키워드 기반 후보 검색 | `ccg search` 또는 MCP `search` | @@ -105,7 +105,7 @@ file/symbol node를 같은 문서 viewer로 엽니다. `--rag-refresh=false`를 사용하십시오. 독립 `ccg rag-index` 명령은 생성 문서와 이미 계산된 community를 사용한 수동 재생성 용도로 남아 있습니다. -그 다음 MCP `retrieve_docs`로 파일 단위 후보와 제한된 Markdown 본문을 matched fields 및 graph evidence와 함께 가져옵니다. `get_rag_tree`로 모듈/커뮤니티 맥락을 펼치고, `get_doc_content`로 특정 생성 문서를 직접 읽은 뒤 정확한 graph 도구로 내려갑니다. `search_docs`와 `ccg search`는 빠른 키워드 또는 어노테이션 매칭에는 유용하지만, 넓은 자연어 질문의 기본 응답 표면으로 보기는 어렵습니다. +그 다음 MCP `search_docs`로 관련 문서를 찾고 `get_doc_content`로 하나를 직접 읽습니다. `get_rag_tree`로 모듈/커뮤니티 맥락을 펼친 뒤 특정 생성 문서로 내려갑니다. `ccg search`는 빠른 키워드 또는 심볼 어노테이션 매칭에는 유용하지만, 넓은 자연어 질문의 기본 응답 표면으로 보기는 어렵습니다. ### 데이터베이스 선택 (Database Choice) diff --git a/guide/ko/mcp-tools.md b/guide/ko/mcp-tools.md index 9328f5d..4b78790 100644 --- a/guide/ko/mcp-tools.md +++ b/guide/ko/mcp-tools.md @@ -103,39 +103,23 @@ CCG는 아직 Prometheus `/metrics` 엔드포인트를 제공하지 않습니다 | `get_rag_tree` | node ID 기반 RAG 문서 트리 탐색 (네임스페이스 지원) | | `get_doc_content` | 문서 파일 내용 확인 (네임스페이스 지원) | | `search_docs` | 키워드로 RAG 문서 트리 검색 (네임스페이스 지원) | -| `retrieve_docs` | DB-backed graph evidence에서 관련 문서를 찾고, matched fields, evidence node, 제한된 Markdown 본문 반환 | -자연어 기반 코드 이해에는 `retrieve_docs`를 먼저 사용하십시오. -`retrieve_docs`는 file-level graph/annotation evidence를 점수화하므로 여러 -키워드가 관련 심볼에 나뉘어 있어도 같은 문서를 후보로 찾고, 제한된 Markdown -본문과 evidence를 반환합니다. Top1 정답을 보장하기보다 Top10 후보를 작게 -좁히는 evidence-driven retrieval layer입니다. `get_rag_tree`는 주변 모듈 구조를 펼칩니다. 먼저 +자연어 기반 코드 이해에는 `search_docs`로 관련 문서를 찾은 뒤 +`get_doc_content`로 하나를 읽으십시오. `search_docs`는 키워드로 RAG 문서 +트리를 검색해 후보 문서를 반환하고, `get_doc_content`는 특정 생성 Markdown +파일을 직접 읽습니다. `get_rag_tree`는 주변 모듈 구조를 펼칩니다. 먼저 인자 없이 호출해 tree를 받고, 반환된 `node_id`를 넘겨 `community`, `package`, `file`, `symbol` 노드로 내려갑니다. 기존 `community_id` -파라미터는 `node_id`의 호환 alias로 유지됩니다. `get_doc_content`는 -특정 생성 Markdown 파일을 직접 읽습니다. 이후 정확한 +파라미터는 `node_id`의 호환 alias로 유지됩니다. 이후 정확한 심볼, edge, flow, 영향 범위가 필요할 때 `get_node`, `query_graph`, `trace_flow`, `get_impact_radius` 같은 graph 도구로 내려가십시오. -`search_docs` 또는 MCP `search`는 넓은 아키텍처 질문이나 "어떻게 -동작하나?" 류의 기본 표면이 아니라, 어노테이션/키워드 기반 후보 검색에 -사용하는 것을 권장합니다. - -`retrieve_docs`의 score는 같은 query 결과 안에서 순위를 정하기 위한 -신호이며, 절대적인 품질 점수가 아닙니다. 서로 다른 query의 score를 직접 -비교하는 용도로는 사용하지 마십시오. 현재 DB-backed 점수는 정확한 심볼/파일 -이름과 high-signal annotation bucket을 우선합니다. exact label, label -contains, `qualified_name`, `@intent`, `@index`, `@domainRule`, `@requires`, -`@ensures`, `@sideEffect`, `@mutates`, `@see`, generic annotation text가 -가중치에 반영되고, 매칭된 고유 query term마다 ranking bonus가 추가됩니다. -`matched_fields`, `matched_terms`, `matches`가 어떤 field, term, graph node가 -근거로 사용됐는지 보여줍니다. +MCP `search`는 심볼 대상 어노테이션/키워드 기반 후보 검색에 사용하십시오. RAG 인덱스 품질은 생성 문서와 비어 있지 않은 community postprocess 결과에 의존합니다. CLI `ccg docs` 명령은 community를 갱신하고 기본 `doc-index.json` 호환 snapshot을 수동 RAG-index workflow용으로 자동 기록합니다. 또한 브라우저 Wiki를 위한 별도 `wiki-index.json` 호환 snapshot도 -기록합니다. Runtime `retrieve_docs`와 Wiki Retrieve는 DB가 설정된 경우 DB를 -사용합니다. MCP만 사용하는 워크플로우에서 community가 없을 수 있으면 `build_rag_index` 전에 +기록합니다. Wiki Retrieve는 DB가 설정된 경우 DB를 사용합니다. MCP만 사용하는 워크플로우에서 community가 없을 수 있으면 `build_rag_index` 전에 `run_postprocess`를 `communities=true`, `flows=false`, `fts=false`로 호출하십시오. diff --git a/guide/mcp-tools.md b/guide/mcp-tools.md index b3a6167..ae4dfd0 100644 --- a/guide/mcp-tools.md +++ b/guide/mcp-tools.md @@ -109,38 +109,24 @@ or broad MCP prompts. | `get_rag_tree` | Navigate RAG document tree by node ID (supports namespace) | | `get_doc_content` | Get documentation file content (supports namespace) | | `search_docs` | Search RAG document tree by keyword (supports namespace) | -| `retrieve_docs` | Retrieve relevant docs from DB-backed graph evidence, with matched fields, evidence nodes, and bounded Markdown content | - -Use `retrieve_docs` as the first stop for natural-language code understanding. -It scores file-level graph and annotation evidence so multi-keyword queries can -match across related symbols and returns bounded Markdown content with evidence. -It is designed for high Top10 recall and low token use, not guaranteed Top1 -ranking. `get_rag_tree` expands the surrounding module structure; call it -without arguments first, then pass `node_id` from the returned tree to drill -into `community`, `package`, `file`, or `symbol` nodes. The older -`community_id` parameter remains a compatibility alias for `node_id`. -`get_doc_content` reads one generated Markdown file directly. After that, use -graph tools such as `get_node`, `query_graph`, `trace_flow`, or + +For natural-language code understanding, use `search_docs` to find relevant +docs, then `get_doc_content` to read one. `search_docs` matches the RAG document +tree by keyword and returns candidate docs, and `get_doc_content` reads one +generated Markdown file directly. `get_rag_tree` expands the surrounding module +structure; call it without arguments first, then pass `node_id` from the +returned tree to drill into `community`, `package`, `file`, or `symbol` nodes. +The older `community_id` parameter remains a compatibility alias for `node_id`. +After that, use graph tools such as `get_node`, `query_graph`, `trace_flow`, or `get_impact_radius` when the task needs exact symbols, edges, flows, or impact -sets. Use `search_docs` or MCP `search` for focused keyword/annotation -candidate search, not as the default surface for broad architecture or "how does -this work?" questions. - -`retrieve_docs` score is a per-query ranking signal, not an absolute quality -metric. It should be compared only between results returned for the same query. -The current DB-backed scoring favors exact symbol/file names and high-signal -annotation buckets: exact label match, label contains, `qualified_name`, -`@intent`, `@index`, `@domainRule`, `@requires`, `@ensures`, `@sideEffect`, -`@mutates`, `@see`, and generic annotation text. Each distinct matched query -term adds another ranking bonus. `matched_fields`, `matched_terms`, and -`matches` explain which fields, terms, and graph nodes contributed evidence. +sets. Use MCP `search` for focused keyword/annotation candidate search over +symbols. RAG index quality depends on generated docs and non-empty community postprocessing. The CLI `ccg docs` command refreshes communities and writes the default `doc-index.json` compatibility snapshot for manual RAG-index workflows. It also writes a separate `wiki-index.json` compatibility snapshot for the -browser Wiki tree. Runtime `retrieve_docs` and Wiki Retrieve use the database -when it is configured. In MCP-only workflows, run `run_postprocess` with +browser Wiki tree. Wiki Retrieve uses the database when it is configured. In MCP-only workflows, run `run_postprocess` with `communities=true`, `flows=false`, and `fts=false` before `build_rag_index` when communities may be missing. diff --git a/internal/mcp/handler_docs.go b/internal/mcp/handler_docs.go index f633891..e423734 100644 --- a/internal/mcp/handler_docs.go +++ b/internal/mcp/handler_docs.go @@ -112,7 +112,7 @@ func (h *handlers) getDocContent(ctx context.Context, request mcp.CallToolReques } // The default namespace maps to the shared docs root, mirroring resolvedRagIndexPath - // and retrieve_docs; only named namespaces resolve under namespaces//. + // and the doc-search path; only named namespaces resolve under namespaces//. var resolvedPath string if namespace != "" && ctxns.Normalize(namespace) != ctxns.DefaultNamespace { if err := validateNamespacePath(namespace, filePath); err != nil { @@ -216,133 +216,3 @@ func (h *handlers) searchDocsFromDB(ctx context.Context, namespace, query string } return results, nil } - -// @intent keep MCP retrieve_docs response decoding compatible while sharing the canonical retrieval DTO. -type retrieveDocsResponse = retrieval.Response - -// @intent keep MCP retrieve_docs result decoding compatible while sharing the canonical retrieval DTO. -type retrieveDocsResult = retrieval.Result - -// @intent build a retrieve_docs response from persisted graph nodes and annotation tags. -// @requires SearchBackend and DB must be configured, and ctx must carry the desired namespace. -// @ensures returned results are grouped one-per-file in first-seen FTS order and keep retrieve_docs' stable JSON shape. -// @sideEffect queries the search backend and may read generated Markdown files for bounded content. -func (h *handlers) retrieveDocsFromDB(ctx context.Context, namespace, query string, limit, contentLimit int, explain bool) (retrieveDocsResponse, error) { - service := retrieval.Service{DB: h.deps.DB, SearchBackend: h.deps.SearchBackend} - retrieved, err := service.FromDBWithOptions(ctx, namespace, query, limit, contentLimit, func(_ context.Context, namespace, docPath string, limit int) (string, bool, error) { - return h.readIndexedDocContent(namespace, docPath, limit) - }, retrieval.Options{Explain: explain}) - if err != nil { - return retrieveDocsResponse{Results: []retrieveDocsResult{}}, err - } - return retrieved, nil -} - -// retrieveDocs retrieves generated Markdown docs using DB-backed graph evidence. -// @intent provide DB-primary document retrieval. -// @param request query is the natural-language retrieval prompt, limit bounds document count, and content_limit bounds each Markdown payload. -// @requires a configured DB and generated docs may exist. -// @ensures returns file-level matches with tree evidence and bounded document content. -// @sideEffect queries the graph DB and reads generated Markdown files. -func (h *handlers) retrieveDocs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - query, err := request.RequireString("query") - if err != nil { - return missingParamResult(err) - } - if strings.TrimSpace(query) == "" { - return mcp.NewToolResultError("query must not be empty"), nil - } - pageReq, err := paging.NormalizeWithDefault(paging.Request{Limit: int(request.GetFloat("limit", 5))}, 5) - if err != nil { - return finalizeToolResult("", newToolResultErr(err.Error())) - } - if pageReq.Limit > 50 { - return mcp.NewToolResultError("limit must be <= 50"), nil - } - contentLimit := int(request.GetFloat("content_limit", 4000)) - if contentLimit < 0 { - return mcp.NewToolResultError("content_limit must be >= 0"), nil - } - if contentLimit > 20000 { - return mcp.NewToolResultError("content_limit must be <= 20000"), nil - } - explain := request.GetBool("explain", false) - - namespace := resolveNamespace(ctx, requestNamespace(request)) - ctx = ctxns.WithNamespace(ctx, namespace) - if h.deps.DB == nil { - return mcp.NewToolResultError("DB is not configured"), nil - } - cacheKey := map[string]any{ - "query": query, - "limit": pageReq.Limit, - "content_limit": contentLimit, - "namespace": namespace, - "explain": explain, - } - return finalizeToolResult(h.cachedExecute(ctx, "retrieve_docs:db:", cacheKey, func() (string, error) { - response, err := h.retrieveDocsFromDB(ctx, namespace, query, pageReq.Limit, contentLimit, explain) - if err != nil { - return "", newToolResultErr(err.Error()) - } - - b, _ := json.Marshal(response) - return string(b), nil - })) -} - -// readIndexedDocContent reads a doc_path under the generated docs root. -// @intent let retrieve_docs return bounded Markdown content while keeping index-provided paths inside a safe root. -// @domainRule namespace doc paths are resolved from that namespace; shared doc paths are resolved from the parent of the RAG index root. -// @sideEffect reads a generated Markdown file. -func (h *handlers) readIndexedDocContent(namespace, docPath string, limit int) (string, bool, error) { - resolvedPath, err := h.resolveIndexedDocPath(namespace, docPath) - if err != nil { - return "", false, err - } - content, err := os.ReadFile(resolvedPath) - if err != nil { - return "", false, fmt.Errorf("read doc_path %q: %w. Run 'ccg docs' to generate documentation files", docPath, err) - } - if len(content) <= limit { - return string(content), false, nil - } - return string(content[:limit]), true, nil -} - -// resolveIndexedDocPath resolves doc-index doc_path values without allowing traversal outside the docs root. -// @intent safely support both relative docs/... paths and absolute doc paths produced by custom docs output directories. -func (h *handlers) resolveIndexedDocPath(namespace, docPath string) (string, error) { - if strings.TrimSpace(docPath) == "" { - return "", fmt.Errorf("doc_path is empty") - } - if namespace != "" { - if err := validateNamespacePath(namespace, docPath); err != nil { - return "", err - } - return h.resolveNamespacePath(namespace, filepath.Clean(docPath), false) - } - indexRoot, err := resolveSafeRoot(h.ragIndexRoot(), false) - if err != nil { - return "", err - } - base := filepath.Dir(indexRoot) - clean := filepath.Clean(docPath) - if filepath.IsAbs(clean) { - if realDocPath, err := filepath.EvalSymlinks(clean); err == nil { - clean = realDocPath - } - rel, err := filepath.Rel(base, clean) - if err != nil { - return "", fmt.Errorf("resolve doc_path: %w", err) - } - if rel == "." { - return base, nil - } - if strings.HasPrefix(rel, "..") { - return "", fmt.Errorf("doc_path %q is outside configured docs root", docPath) - } - return safePathUnderRoot(base, rel, "doc_path", false, false) - } - return safePathUnderRoot(base, clean, "doc_path", false, false) -} diff --git a/internal/mcp/handler_docs_test.go b/internal/mcp/handler_docs_test.go index dfa6d3d..0e98d7c 100644 --- a/internal/mcp/handler_docs_test.go +++ b/internal/mcp/handler_docs_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "os" "path/filepath" - "slices" "strings" "testing" @@ -327,181 +326,6 @@ func TestSearchDocs_UsesDBWhenDocIndexFileExists(t *testing.T) { } } -func TestRetrieveDocs_DBFallbackSucceedsWithoutDocIndex(t *testing.T) { - deps := setupTestDeps(t) - tmpDir := t.TempDir() - docsDir := filepath.Join(tmpDir, "docs") - deps.RagIndexDir = filepath.Join(tmpDir, ".ccg") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "billing.Processor", "Processor", "internal/billing/processor.go", model.TagIntent, "payment settlement workflow", "payment settlement workflow") - writeRetrieveDocsMarkdown(t, docsDir, "internal/billing/processor.go", "# processor.go\n\npayment settlement workflow docs\n") - rebuildRetrieveDocsSearchBackend(t, deps, ctxns.DefaultNamespace) - - result := callTool(t, deps, "retrieve_docs", map[string]any{ - "query": "payment settlement", - "limit": float64(5), - "content_limit": float64(2000), - }) - if result.IsError { - t.Fatalf("retrieve_docs DB fallback error: %v", getTextContent(result)) - } - - response := decodeRetrieveDocsResponse(t, result) - if len(response.Results) != 1 { - t.Fatalf("results = %d, want 1: %#v", len(response.Results), response.Results) - } - got := response.Results[0] - if got.DocPath != "docs/internal/billing/processor.go.md" { - t.Fatalf("doc_path = %q, want docs/internal/billing/processor.go.md", got.DocPath) - } - if !strings.Contains(got.Content, "payment settlement workflow docs") { - t.Fatalf("content = %q", got.Content) - } -} - -func TestRetrieveDocs_DBFallbackIsNamespaceIsolated(t *testing.T) { - deps := setupTestDeps(t) - tmpDir := t.TempDir() - deps.NamespaceRoot = filepath.Join(tmpDir, "namespaces") - deps.RagIndexDir = filepath.Join(tmpDir, ".ccg") - alpha := "alpha-service" - beta := "beta-service" - seedRetrieveDocsDBFallbackNode(t, deps, alpha, "alpha.Checkout", "Checkout", "checkout.go", model.TagIntent, "sharedtenant alpha checkout", "sharedtenant alpha checkout") - seedRetrieveDocsDBFallbackNode(t, deps, beta, "beta.Checkout", "Checkout", "checkout.go", model.TagIntent, "sharedtenant beta checkout", "sharedtenant beta checkout") - writeRetrieveDocsMarkdown(t, filepath.Join(deps.NamespaceRoot, alpha, "docs"), "checkout.go", "# checkout\n\nalpha checkout docs\n") - writeRetrieveDocsMarkdown(t, filepath.Join(deps.NamespaceRoot, beta, "docs"), "checkout.go", "# checkout\n\nbeta checkout docs\n") - rebuildRetrieveDocsSearchBackend(t, deps, alpha) - rebuildRetrieveDocsSearchBackend(t, deps, beta) - - result := callTool(t, deps, "retrieve_docs", map[string]any{ - "namespace": alpha, - "query": "sharedtenant checkout", - "limit": float64(5), - "content_limit": float64(2000), - }) - if result.IsError { - t.Fatalf("retrieve_docs namespace DB fallback error: %v", getTextContent(result)) - } - - response := decodeRetrieveDocsResponse(t, result) - if len(response.Results) != 1 { - t.Fatalf("results = %d, want 1: %#v", len(response.Results), response.Results) - } - if !strings.Contains(response.Results[0].Content, "alpha checkout docs") { - t.Fatalf("expected alpha docs, got %q", response.Results[0].Content) - } - if strings.Contains(response.Results[0].Content, "beta checkout docs") || strings.Contains(response.Results[0].Summary, "beta") { - t.Fatalf("namespace leaked beta result: %#v", response.Results[0]) - } -} - -func TestRetrieveDocs_DBFallbackMissingMarkdownDoesNotFail(t *testing.T) { - deps := setupTestDeps(t) - tmpDir := t.TempDir() - deps.RagIndexDir = filepath.Join(tmpDir, ".ccg") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "docs.Missing", "Missing", "internal/docs/missing.go", model.TagIntent, "missing markdown fallback", "missing markdown fallback") - rebuildRetrieveDocsSearchBackend(t, deps, ctxns.DefaultNamespace) - - result := callTool(t, deps, "retrieve_docs", map[string]any{ - "query": "missing markdown", - "limit": float64(5), - "content_limit": float64(2000), - }) - if result.IsError { - t.Fatalf("retrieve_docs should not fail when DB fallback markdown is missing: %v", getTextContent(result)) - } - - response := decodeRetrieveDocsResponse(t, result) - if len(response.Results) != 1 { - t.Fatalf("results = %d, want 1: %#v", len(response.Results), response.Results) - } - if response.Results[0].Content != "" { - t.Fatalf("missing markdown content = %q, want empty", response.Results[0].Content) - } - if response.Results[0].DocPath != "docs/internal/docs/missing.go.md" { - t.Fatalf("doc_path = %q, want docs/internal/docs/missing.go.md", response.Results[0].DocPath) - } -} - -func TestRetrieveDocs_DBFallbackAnnotationOnlyMatchIncludesAnnotationBucket(t *testing.T) { - deps := setupTestDeps(t) - tmpDir := t.TempDir() - deps.RagIndexDir = filepath.Join(tmpDir, ".ccg") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "policy.Guard", "Guard", "internal/policy/guard.go", model.TagDomainRule, "breakglass approval required", "policy guard searchable") - rebuildRetrieveDocsSearchBackend(t, deps, ctxns.DefaultNamespace) - - result := callTool(t, deps, "retrieve_docs", map[string]any{"query": "breakglass", "limit": float64(5), "content_limit": float64(0)}) - if result.IsError { - t.Fatalf("retrieve_docs annotation-only DB fallback error: %v", getTextContent(result)) - } - - response := decodeRetrieveDocsResponse(t, result) - if len(response.Results) != 1 { - t.Fatalf("results = %d, want 1: %#v", len(response.Results), response.Results) - } - if !retrieveDocsHasField(response.Results[0].MatchedFields, string(model.TagDomainRule)) { - t.Fatalf("matched_fields missing domainRule annotation bucket: %#v", response.Results[0].MatchedFields) - } -} - -func TestRetrieveDocs_DBFallbackHonorsLimitAfterFileGrouping(t *testing.T) { - deps := setupTestDeps(t) - tmpDir := t.TempDir() - deps.RagIndexDir = filepath.Join(tmpDir, ".ccg") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "alpha.First", "First", "internal/alpha/file.go", model.TagIntent, "groupterm first", "groupterm first") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "alpha.Second", "Second", "internal/alpha/file.go", model.TagIntent, "groupterm second", "groupterm second") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "beta.Only", "Only", "internal/beta/file.go", model.TagIntent, "groupterm beta", "groupterm beta") - rebuildRetrieveDocsSearchBackend(t, deps, ctxns.DefaultNamespace) - - result := callTool(t, deps, "retrieve_docs", map[string]any{"query": "groupterm", "limit": float64(1), "content_limit": float64(0)}) - if result.IsError { - t.Fatalf("retrieve_docs DB fallback limit error: %v", getTextContent(result)) - } - - response := decodeRetrieveDocsResponse(t, result) - if len(response.Results) != 1 { - t.Fatalf("results = %d, want exactly 1 file group: %#v", len(response.Results), response.Results) - } - if len(response.Results[0].Matches) != 2 { - t.Fatalf("first file group should keep both node matches, got %#v", response.Results[0].Matches) - } -} - -func TestRetrieveDocs_DBFallbackResponseShapeStableByDefault(t *testing.T) { - deps := setupTestDeps(t) - tmpDir := t.TempDir() - deps.RagIndexDir = filepath.Join(tmpDir, ".ccg") - seedRetrieveDocsDBFallbackNode(t, deps, ctxns.DefaultNamespace, "stable.Shape", "Shape", "internal/stable/shape.go", model.TagIntent, "stable shape", "stable shape") - rebuildRetrieveDocsSearchBackend(t, deps, ctxns.DefaultNamespace) - - result := callTool(t, deps, "retrieve_docs", map[string]any{"query": "stable", "limit": float64(5), "content_limit": float64(0)}) - if result.IsError { - t.Fatalf("retrieve_docs DB fallback shape error: %v", getTextContent(result)) - } - jsonText := getTextContent(result) - for _, key := range []string{"expanded_terms", "field_scores", "literal_score", "expansion_score"} { - if strings.Contains(jsonText, key) { - t.Fatalf("default response must omit %q diagnostic key, got %s", key, jsonText) - } - } - - response := decodeRetrieveDocsResponse(t, result) - if len(response.Results) != 1 { - t.Fatalf("results = %d, want 1: %#v", len(response.Results), response.Results) - } - got := response.Results[0] - if got.ID == "" || got.Label == "" || got.Kind != "file" || got.DocPath == "" || len(got.Path) == 0 || len(got.MatchedTerms) == 0 || len(got.MatchedFields) == 0 { - t.Fatalf("unstable DB fallback response shape: %#v", got) - } -} - -func TestRetrieveDocs_RejectsLimitAboveMax(t *testing.T) { - deps := setupTestDeps(t) - result := callTool(t, deps, "retrieve_docs", map[string]any{"query": "auth", "limit": float64(51)}) - if !result.IsError { - t.Fatal("expected retrieve_docs to reject limit above max") - } -} - func TestSearchDocs_RejectsInvalidNamespace(t *testing.T) { deps := setupTestDeps(t) result := callTool(t, deps, "search_docs", map[string]any{"query": "auth", "namespace": "../outside"}) @@ -684,15 +508,6 @@ func writeRetrieveDocsMarkdown(t *testing.T, docsDir, filePath, content string) } } -func decodeRetrieveDocsResponse(t *testing.T, result *mcp.CallToolResult) retrieveDocsResponse { - t.Helper() - var response retrieveDocsResponse - if err := json.Unmarshal([]byte(getTextContent(result)), &response); err != nil { - t.Fatalf("unmarshal retrieve response: %v", err) - } - return response -} - func decodeSearchDocsResults(t *testing.T, result *mcp.CallToolResult) []ragindex.SearchResult { t.Helper() var results []ragindex.SearchResult @@ -701,7 +516,3 @@ func decodeSearchDocsResults(t *testing.T, result *mcp.CallToolResult) []raginde } return results } - -func retrieveDocsHasField(fields []string, want string) bool { - return slices.Contains(fields, want) -} diff --git a/internal/mcp/tools_docs.go b/internal/mcp/tools_docs.go index 5f4a0af..1d76219 100644 --- a/internal/mcp/tools_docs.go +++ b/internal/mcp/tools_docs.go @@ -27,16 +27,5 @@ func docsTools(h *handlers) []server.ServerTool { ), Handler: h.searchDocs, }, - { - Tool: mcp.NewTool("retrieve_docs", - mcp.WithDescription("Retrieve relevant generated docs for a natural-language or multi-keyword query. Uses DB-backed graph evidence and returns matched evidence plus bounded document content."), - mcp.WithString("query", mcp.Description("Natural-language or multi-keyword retrieval query"), mcp.Required()), - mcp.WithNumber("limit", mcp.Description("Maximum number of document results (default: 5, max: 50)")), - mcp.WithNumber("content_limit", mcp.Description("Maximum bytes of Markdown content per result (default: 4000, max: 20000; use 0 to omit content)")), - mcp.WithString("namespace", mcp.Description("Namespace. When set, retrieves from that namespace's DB rows and generated docs.")), - mcp.WithBoolean("explain", mcp.Description("When true, include available per-result diagnostics such as field_scores, literal_score, expanded_terms, and expansion_score (default: false).")), - ), - Handler: h.retrieveDocs, - }, } } diff --git a/internal/ragindex/builder.go b/internal/ragindex/builder.go index 85f24a5..aaf36ec 100644 --- a/internal/ragindex/builder.go +++ b/internal/ragindex/builder.go @@ -118,24 +118,19 @@ type SearchResult struct { } // RetrieveResult represents one document candidate selected from tree-aware query matching. -// @intent return file-level RAG retrieval candidates with the matched tree evidence that caused the hit, including which annotation buckets contributed. Optional Phase 2 diagnostics (ExpandedTerms, FieldScores, LiteralScore, ExpansionScore) are emitted only when RetrieveOptions.Explain is true and stay omitempty so default responses keep the shape backward-compatible. +// @intent return file-level doc candidates with the matched tree evidence that caused the hit, including which annotation buckets contributed. type RetrieveResult struct { - ID string `json:"id"` - Label string `json:"label"` - Kind string `json:"kind"` - Summary string `json:"summary"` - DocPath string `json:"doc_path"` - Path []string `json:"path"` - Score int `json:"score"` - MatchedTerms []string `json:"matched_terms"` - MatchedFields []string `json:"matched_fields"` - Matches []SearchResult `json:"matches,omitempty"` - ExpandedTerms []string `json:"expanded_terms,omitempty"` - FieldScores map[string]int `json:"field_scores,omitempty"` - LiteralScore int `json:"literal_score,omitempty"` - ExpansionScore int `json:"expansion_score,omitempty"` - - wholeWordHits int `json:"-"` + ID string `json:"id"` + Label string `json:"label"` + Kind string `json:"kind"` + Summary string `json:"summary"` + DocPath string `json:"doc_path"` + Path []string `json:"path"` + Score int `json:"score"` + MatchedTerms []string `json:"matched_terms"` + MatchedFields []string `json:"matched_fields"` + Matches []SearchResult `json:"matches,omitempty"` + wholeWordHits int `json:"-"` } // Search는 root 트리를 DFS로 순회하며 query를 label, summary, search_text에서 diff --git a/internal/retrieval/helpers.go b/internal/retrieval/helpers.go index 80d9185..2a819c2 100644 --- a/internal/retrieval/helpers.go +++ b/internal/retrieval/helpers.go @@ -25,14 +25,14 @@ func IsRetrievableNodeKind(kind model.NodeKind) bool { } // DBFileGroup은 파일 경로별로 묶인 DB 후보 노드 그룹이다. -// @intent retrieve_docs DB-backed 결과가 파일 단위 세분성을 유지하도록 후보를 그룹화한다. +// @intent doc retrieval DB-backed 결과가 파일 단위 세분성을 유지하도록 후보를 그룹화한다. type DBFileGroup struct { FilePath string Nodes []model.Node } // DBCandidateLimit은 그룹화 후 파일 단위 결과 수가 충분하도록 FTS 후보 수를 결정한다. -// @intent retrieve_docs는 파일당 하나의 결과를 반환하므로 후보 수는 최종 파일 한도를 초과해야 하되 무한정 커지면 안 된다. +// @intent doc retrieval는 파일당 하나의 결과를 반환하므로 후보 수는 최종 파일 한도를 초과해야 하되 무한정 커지면 안 된다. // @domainRule 후보 수는 최소 dbCandidateFloor, 최대 dbCandidateCap으로 제한한다. func DBCandidateLimit(limit int) int { return min(max(limit*10, dbCandidateFloor), dbCandidateCap) @@ -189,15 +189,9 @@ func GroupCandidatesByFile(nodes []model.Node, limit int) ([]DBFileGroup, []uint return groups, nodeIDs } -// BuildDBResult는 그룹화된 DB 검색 히트와 구조화된 어노테이션으로부터 파일 수준 retrieve_docs 후보를 도출한다. -// @intent 그룹화된 DB 검색 히트와 구조화된 어노테이션으로부터 파일 수준 retrieve_docs 후보를 도출한다. +// BuildDBResult는 그룹화된 DB 검색 히트와 구조화된 어노테이션으로부터 파일 수준 문서 후보를 도출한다. +// @intent score DB-backed doc candidates from structured annotation buckets while preserving backend rank in response order. func BuildDBResult(group DBFileGroup, annotations map[uint]*model.Annotation, terms []string, index int) ragindex.RetrieveResult { - return BuildDBResultWithOptions(group, annotations, terms, index, Options{}) -} - -// BuildDBResultWithOptions derives one file-level retrieve_docs candidate and optional diagnostics. -// @intent score DB-backed retrieve_docs candidates from structured annotation buckets while preserving backend rank in response order. -func BuildDBResultWithOptions(group DBFileGroup, annotations map[uint]*model.Annotation, terms []string, index int, opts Options) ragindex.RetrieveResult { score, fieldScores, matchedTerms := ScoreDBFields(group.Nodes, annotations, terms) fields := sortedFieldNames(fieldScores) if len(fields) == 0 { @@ -207,7 +201,7 @@ func BuildDBResultWithOptions(group DBFileGroup, annotations map[uint]*model.Ann matchedTerms = terms } summary := DBSummary(group.Nodes, annotations) - result := ragindex.RetrieveResult{ + return ragindex.RetrieveResult{ ID: "file:" + group.FilePath, Label: filepath.Base(group.FilePath), Kind: "file", @@ -218,28 +212,10 @@ func BuildDBResultWithOptions(group DBFileGroup, annotations map[uint]*model.Ann MatchedTerms: matchedTerms, MatchedFields: fields, } - if opts.Explain { - result.FieldScores = fieldScores - result.LiteralScore = score - } - return result -} - -// MatchedFields는 DB-backed 파일 히트를 설명하는 노드 메타데이터 또는 어노테이션 버킷을 보고한다. -// @intent DB-backed 매칭 필드 진단을 위해 어떤 노드 메타데이터 또는 어노테이션 버킷이 파일 히트를 설명하는지 보고한다. -func MatchedFields(nodes []model.Node, annotations map[uint]*model.Annotation, terms []string) []string { - return sortedFieldNames(ScoreDBFieldBreakdown(nodes, annotations, terms)) -} - -// ScoreDBFieldBreakdown reports weighted retrieval buckets without the distinct-term bonus. -// @intent expose DB retrieval bucket scores for tests and explain-mode diagnostics. -func ScoreDBFieldBreakdown(nodes []model.Node, annotations map[uint]*model.Annotation, terms []string) map[string]int { - _, fields, _ := scoreDBFields(nodes, annotations, terms) - return fields } // ScoreDBFields scores DB-backed retrieval evidence using PageIndex-style structured bucket weights. -// @intent rank high-signal annotation buckets above generic text fallback for DB-backed retrieve_docs. +// @intent rank high-signal annotation buckets above generic text fallback for DB-backed doc retrieval. func ScoreDBFields(nodes []model.Node, annotations map[uint]*model.Annotation, terms []string) (int, map[string]int, []string) { return scoreDBFields(nodes, annotations, terms) } @@ -314,7 +290,7 @@ func scoreDBFields(nodes []model.Node, annotations map[uint]*model.Annotation, t return score, fieldScores, sortedTerms(matchedTerms) } -// @intent map persisted annotation tag kinds to retrieve_docs matched_fields names. +// @intent map persisted annotation tag kinds to doc retrieval matched_fields names. func dbFieldName(kind model.TagKind) string { if kind == model.TagIndex { return "index_summary" @@ -350,7 +326,7 @@ func sortedFieldNames(fieldScores map[string]int) []string { return fields } -// @intent produce stable matched_terms ordering for DB-backed retrieve_docs responses. +// @intent produce stable matched_terms ordering for DB-backed doc retrieval responses. func sortedTerms(terms map[string]struct{}) []string { out := make([]string, 0, len(terms)) for term := range terms { diff --git a/internal/retrieval/service.go b/internal/retrieval/service.go index eea7010..8818d22 100644 --- a/internal/retrieval/service.go +++ b/internal/retrieval/service.go @@ -13,27 +13,12 @@ import ( "github.com/tae2089/code-context-graph/internal/model" ) -// FromDB builds a retrieve_docs response from persisted graph nodes and annotations. -// @intent orchestrate DB-backed retrieve_docs lookup through search backend first and namespace-scoped scan supplementation second. +// FromDB builds a doc search/retrieval response from persisted graph nodes and annotations. +// @intent orchestrate DB-backed doc lookup through search backend first and namespace-scoped scan supplementation second. // @requires Service.DB must be configured and limit must be positive for meaningful results. // @ensures results are grouped one-per-file, annotation-enriched, and optionally populated with bounded content. // @sideEffect queries the configured database and may invoke an injected content reader. func (s *Service) FromDB(ctx context.Context, namespace, query string, limit, contentLimit int, read ContentReader) (Response, error) { - return s.FromDBWithOptions(ctx, namespace, query, limit, contentLimit, read, Options{}) -} - -// Options controls optional DB-backed retrieval diagnostics. -// @intent let DB-backed retrieve_docs expose opt-in scoring diagnostics without changing the default response shape. -type Options struct { - Explain bool -} - -// FromDBWithOptions builds a retrieve_docs response from DB data and optional diagnostics. -// @intent support DB-primary retrieve_docs while preserving doc-index-compatible explain response fields. -// @requires Service.DB must be configured and limit must be positive for meaningful results. -// @ensures results are grouped one-per-file, annotation-enriched, and optionally populated with bounded content. -// @sideEffect queries the configured database and may invoke an injected content reader. -func (s *Service) FromDBWithOptions(ctx context.Context, namespace, query string, limit, contentLimit int, read ContentReader, opts Options) (Response, error) { response := Response{Results: []Result{}} if s.DB == nil { return response, fmt.Errorf("DB not configured") @@ -74,7 +59,7 @@ func (s *Service) FromDBWithOptions(ctx context.Context, namespace, query string terms := MatchedTerms(query) response.Results = make([]Result, 0, len(groups)) for idx, group := range groups { - result := Result{RetrieveResult: BuildDBResultWithOptions(group, annotations, terms, idx, opts)} + result := Result{RetrieveResult: BuildDBResult(group, annotations, terms, idx)} result.Matches = DBMatches(group.Nodes, annotations) response.Results = append(response.Results, result) } @@ -97,7 +82,7 @@ func (s *Service) FromDBWithOptions(ctx context.Context, namespace, query string } // @intent ask the configured FTS search backend for a bounded candidate set while letting callers supplement from DB scan when search is unavailable. -// @domainRule FTS query errors are non-fatal for retrieve_docs because DB scan can still produce namespace-scoped file candidates. +// @domainRule FTS query errors are non-fatal for doc retrieval because DB scan can still produce namespace-scoped file candidates. func (s *Service) searchCandidates(ctx context.Context, query string, limit int) []model.Node { if s.SearchBackend == nil { return nil @@ -110,7 +95,7 @@ func (s *Service) searchCandidates(ctx context.Context, query string, limit int) } // scanDBCandidates collects supplemental candidates by scanning namespace-scoped nodes and annotations. -// @intent supplement retrieve_docs candidates when backend FTS is missing, failing, or too narrow. +// @intent supplement doc retrieval candidates when backend FTS is missing, failing, or too narrow. func (s *Service) scanDBCandidates(ctx context.Context, namespace, query string) ([]model.Node, error) { terms := MatchedTerms(query) if len(terms) == 0 { @@ -125,11 +110,11 @@ func (s *Service) scanDBCandidates(ctx context.Context, namespace, query string) Order("file_path ASC, qualified_name ASC, id ASC"). Limit(scanRowCap). Find(&nodes).Error; err != nil { - return nil, fmt.Errorf("retrieve docs DB candidates: %w", err) + return nil, fmt.Errorf("doc retrieval DB candidates: %w", err) } if len(nodes) == scanRowCap { // The scan hit its ceiling: matching nodes sorting after the cap are not considered. - slog.WarnContext(ctx, "retrieve_docs fallback scan truncated at cap; some matches may be omitted", + slog.WarnContext(ctx, "doc retrieval fallback scan truncated at cap; some matches may be omitted", "namespace", namespace, "cap", scanRowCap) } if len(nodes) == 0 { @@ -231,7 +216,7 @@ func (s *Service) batchAnnotations(ctx context.Context, namespace string, nodeID Where("annotations.node_id IN ? AND nodes.namespace = ?", nodeIDs, namespace). Preload("Tags"). Find(&rows).Error; err != nil { - return nil, fmt.Errorf("batch retrieve_docs annotations: %w", err) + return nil, fmt.Errorf("batch doc retrieval annotations: %w", err) } for i := range rows { annotations[rows[i].NodeID] = &rows[i] diff --git a/skills/ccg-annotate/SKILL.md b/skills/ccg-annotate/SKILL.md index 3ed730c..2dcba88 100644 --- a/skills/ccg-annotate/SKILL.md +++ b/skills/ccg-annotate/SKILL.md @@ -142,7 +142,7 @@ ccg build . ccg search "graph viewer ccg ref node focus" ``` -For MCP/Web UI retrieval, use `retrieve_docs` or the Wiki Retrieve mode. If the +For MCP/Web UI retrieval, use `search_docs` + `get_doc_content` or the Wiki Retrieve mode. If the expected file is missing, prefer improving the precise `@index`, `@intent`, `@domainRule`, or `@see` evidence on that file over changing global scoring. diff --git a/skills/ccg-docs/SKILL.md b/skills/ccg-docs/SKILL.md index 76c0b6f..fb1027d 100644 --- a/skills/ccg-docs/SKILL.md +++ b/skills/ccg-docs/SKILL.md @@ -11,9 +11,9 @@ Generate Markdown wiki from code graph. Build RAG tree for fast AI exploration. | Task | Tool | | ------------------------------------------------------ | ---------------------------------------- | -| Broad natural-language question ("how does auth work?") | `retrieve_docs` first | +| Broad natural-language question ("how does auth work?") | `search_docs`, then `get_doc_content` | | Semantic keyword search ("payment code") | `ccg search` (`/ccg` skill) | -| Module/domain exploration ("payment module structure") | `get_rag_tree` after `retrieve_docs` | +| Module/domain exploration ("payment module structure") | `get_rag_tree` after `search_docs` | | Exact generated doc body | `get_doc_content` | | Exact signature/location | `query_graph`, `get_node` (`/ccg` skill) | | Dynamic call tracing | `trace_flow` (`/ccg-analyze`) | @@ -41,7 +41,7 @@ and call `build_rag_index` again. ## RAG Usage Pattern ``` -retrieve_docs("auth flow") # broad question → bounded docs + evidence +search_docs("auth flow") # broad question → matching docs + evidence get_rag_tree(community_id) # expand specific community get_doc_content(doc_path) # fetch exact Markdown body search_docs("auth") # focused keyword → tree node candidates @@ -106,14 +106,13 @@ If RAG answer quality is low, usually one of: | `get_rag_tree` | Navigate and verify the community tree | | `get_doc_content` | Fetch Markdown body | | `search_docs` | Keyword search the RAG tree | -| `retrieve_docs` | Retrieve ranked docs with tree evidence and bounded Markdown content | When the user explicitly asks to build the RAG index through MCP, call `build_rag_index` through MCP rather than substituting the CLI command. Use CLI only when the user asks for a terminal command or when MCP is unavailable. -Use `retrieve_docs` as the default MCP read path for architecture or "how does -this work?" questions. Use `search_docs` when the user needs a focused keyword +Use `search_docs` to find relevant docs for architecture or "how does +this work?" questions, then `get_doc_content` to read them. Use `search_docs` when the user needs a focused keyword candidate list rather than a synthesized documentation context. ## Prerequisites diff --git a/skills/ccg/SKILL.md b/skills/ccg/SKILL.md index 162c095..b2857c6 100644 --- a/skills/ccg/SKILL.md +++ b/skills/ccg/SKILL.md @@ -14,7 +14,7 @@ ccg is a Tree-sitter-based code graph tool. **Complementary to Grep/Read, not a | "Where is X?" — simple location lookup | Grep + Read | Faster and cheaper than ccg | | "Find code related to X" — semantic search | `ccg search` | Annotation/keyword semantic match | | "What's affected if I change X?" | `/ccg-analyze` | Graph traversal | -| "Understand structure/architecture" | `/ccg-docs` (RAG) | `retrieve_docs` first, then tree | +| "Understand structure/architecture" | `/ccg-docs` (RAG) | `search_docs`, then `get_doc_content` | | "Document intent/rules in code" | `/ccg-annotate` | AI annotation workflow | | "Manage multiple service codebases" | `/ccg-namespace` | MSA namespace isolation | @@ -76,7 +76,7 @@ ccg build . ccg docs --out docs ``` -Then use MCP `retrieve_docs` for bounded Markdown evidence, `get_rag_tree` to +Then use MCP `search_docs` to find docs and `get_doc_content` to read them, `get_rag_tree` to expand module context, and only then `query_graph`, `get_node`, or `trace_flow` for exact symbols and relationships.