Skip to content

Commit 8c9d446

Browse files
committed
Improve wiki DB fallback rendering
1 parent 03f25e5 commit 8c9d446

9 files changed

Lines changed: 252 additions & 25 deletions

File tree

Makefile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ BASE_LDFLAGS = -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=
55
LDFLAGS = -s -w $(BASE_LDFLAGS)
66
WIKI_ADDR ?= 127.0.0.1:8080
77
WIKI_DB ?= ccg.db
8+
WIKI_REPO ?= .
89
WIKI_TOKEN ?=
910

10-
.PHONY: build release build-debug build-json install vet test test-integration-helpers wiki-build wiki-run clean
11+
.PHONY: build release build-debug build-json install vet test test-integration-helpers wiki-build wiki-db wiki-docs wiki-run wiki-run-indexed clean
1112

1213
build: release
1314

@@ -38,8 +39,17 @@ test-integration-helpers:
3839
wiki-build:
3940
cd web/wiki && npm ci && npm run build
4041

41-
wiki-run: wiki-build
42+
wiki-db:
4243
CGO_ENABLED=1 go run -tags "fts5" ./cmd/ccg --db-driver sqlite --db-dsn '$(WIKI_DB)' migrate
44+
CGO_ENABLED=1 go run -tags "fts5" ./cmd/ccg --db-driver sqlite --db-dsn '$(WIKI_DB)' build '$(WIKI_REPO)'
45+
46+
wiki-docs: wiki-db
47+
CGO_ENABLED=1 go run -tags "fts5" ./cmd/ccg --db-driver sqlite --db-dsn '$(WIKI_DB)' docs --out docs
48+
49+
wiki-run: wiki-build wiki-db
50+
CGO_ENABLED=1 go run -tags "fts5" ./cmd/ccg-server --db-driver sqlite --db-dsn '$(WIKI_DB)' --http-addr '$(WIKI_ADDR)' --http-bearer-token '$(WIKI_TOKEN)' --wiki-dir web/wiki/dist
51+
52+
wiki-run-indexed: wiki-build wiki-docs
4353
CGO_ENABLED=1 go run -tags "fts5" ./cmd/ccg-server --db-driver sqlite --db-dsn '$(WIKI_DB)' --http-addr '$(WIKI_ADDR)' --http-bearer-token '$(WIKI_TOKEN)' --wiki-dir web/wiki/dist
4454

4555
clean:

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ ccg search "payment" # finds functions with @intent/@domainRule about payment
6969
# Build docs and the default vectorless RAG index for agent-oriented exploration
7070
ccg docs --out docs
7171

72-
# Serve the browser Wiki UI from built assets
72+
# Serve the browser Wiki UI from built assets; builds the graph and uses DB-backed fallback
7373
make wiki-run
7474

7575
# Graph statistics
@@ -119,7 +119,8 @@ size stays small.
119119
The Wiki is meant for developers and agents inspecting a generated codebase:
120120

121121
- Tree navigation over folders, packages, files, and annotated symbols
122-
- Keyword search and PageIndex-style `retrieve_docs` over `doc-index.json`
122+
- Keyword search and PageIndex-style `retrieve_docs` over generated indexes,
123+
with DB-backed fallback when index files have not been generated
123124
- Rich symbol detail cards from CCG annotations even when a symbol has no
124125
generated Markdown file
125126
- Context Tray for collecting files and doc-less symbols into one Markdown
@@ -130,11 +131,12 @@ The Wiki is meant for developers and agents inspecting a generated codebase:
130131
Local development shortcut:
131132

132133
```bash
133-
ccg build .
134-
ccg docs --out docs
135134
make wiki-run
136135
```
137136

137+
Use `make wiki-run-indexed` when you also want generated Markdown,
138+
`wiki-index.json`, and `doc-index.json` before starting the server.
139+
138140
For self-hosted deployments, run `ccg-server --wiki-dir <dist-dir>` and protect
139141
`/wiki/api/*` with the same bearer token policy used for `/mcp`. See
140142
[Docker](guide/docker.md#wiki-ui) and [Runtime Layout](guide/runtime-layout.md)

guide/development.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ Makefile shortcuts:
1313
make build # build stripped ccg and ccg-server binaries (same as make release)
1414
make release # stripped build with embedded version/commit/date
1515
make build-debug # unstripped ccg and ccg-server binaries with embedded version/commit/date
16-
make wiki-run # build Wiki UI, migrate local SQLite DB, run ccg-server with /wiki
16+
make wiki-db # migrate the local Wiki DB and build the graph from WIKI_REPO
17+
make wiki-run # build Wiki UI, build graph, run ccg-server with DB-backed Wiki fallback
18+
make wiki-run-indexed # build Wiki UI, build graph, generate docs/indexes, then run ccg-server
1719
```
1820

1921
`make wiki-run` defaults to `127.0.0.1:8080` and `ccg.db`. Override values with
20-
`WIKI_ADDR`, `WIKI_DB`, and optionally `WIKI_TOKEN`:
22+
`WIKI_ADDR`, `WIKI_DB`, `WIKI_REPO`, and optionally `WIKI_TOKEN`:
2123

2224
```bash
2325
WIKI_ADDR=127.0.0.1:18080 WIKI_TOKEN=dev-token make wiki-run

guide/ko/development.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ Makefile 단축 명령어:
1515
make build # stripped ccg 및 ccg-server 바이너리 빌드 (make release와 동일)
1616
make release # 버전/커밋/날짜 정보가 포함된 stripped 빌드
1717
make build-debug # 버전/커밋/날짜 정보가 포함된 unstripped ccg 및 ccg-server 바이너리 빌드
18-
make wiki-run # Wiki UI 빌드, 로컬 SQLite DB 마이그레이션, /wiki 포함 ccg-server 실행
18+
make wiki-db # 로컬 Wiki DB 마이그레이션 및 WIKI_REPO 그래프 빌드
19+
make wiki-run # Wiki UI 빌드, 그래프 빌드, DB-backed fallback으로 ccg-server 실행
20+
make wiki-run-indexed # Wiki UI 빌드, 그래프/문서/index 생성 후 ccg-server 실행
1921
```
2022

2123
`make wiki-run`은 기본값으로 `127.0.0.1:8080``ccg.db`를 사용합니다.
22-
`WIKI_ADDR`, `WIKI_DB`, 필요 시 `WIKI_TOKEN`으로 값을 바꿀 수 있습니다:
24+
`WIKI_ADDR`, `WIKI_DB`, `WIKI_REPO`, 필요 시 `WIKI_TOKEN`으로 값을 바꿀 수 있습니다:
2325

2426
```bash
2527
WIKI_ADDR=127.0.0.1:18080 WIKI_TOKEN=dev-token make wiki-run

internal/mcp/tools_docs.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ func docsTools(h *handlers) []server.ServerTool {
2121
},
2222
{
2323
Tool: mcp.NewTool("get_rag_tree",
24-
mcp.WithDescription("Get the RAG document tree for navigation. Call without arguments first, then pass node_id to drill into a community, package, file, or symbol node."),
25-
mcp.WithString("node_id", mcp.Description("Tree node ID as shown in the tree (e.g. 'community:internal/analysis', 'package:internal/core', or 'file:internal/core/runtime.go'). Omit to get the full tree.")),
24+
mcp.WithDescription("Navigate the RAG document tree after retrieve_docs/search_docs has identified a useful area. Prefer depth=1 for overview or pass node_id with a small depth to expand nearby context; avoid unbounded full-tree calls on large namespaces."),
25+
mcp.WithString("node_id", mcp.Description("Tree node ID as shown in search/retrieve/tree results (e.g. 'community:internal/analysis', 'package:internal/core', or 'file:internal/core/runtime.go'). Omit only for a bounded overview with depth set.")),
2626
mcp.WithString("community_id", mcp.Description("Deprecated alias for node_id; accepts any tree node ID for compatibility.")),
27-
mcp.WithNumber("depth", mcp.Description("Maximum tree depth to return (1=top-level communities/packages, 2=plus child packages/files). Default: 0 (unlimited).")),
27+
mcp.WithNumber("depth", mcp.Description("Maximum tree depth to return (1=top-level communities/packages, 2=plus child packages/files). Recommended when node_id is omitted. Default: 0 (unlimited, only for small namespaces).")),
2828
mcp.WithString("namespace", mcp.Description("Namespace. When set, reads doc-index.json from the namespace-specific index directory.")),
2929
),
3030
Handler: h.getRagTree,

internal/wikiserver/server.go

Lines changed: 200 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -976,26 +976,215 @@ func symbolMatches(name, qualifiedName, want string) bool {
976976
strings.HasSuffix(qualifiedName, "::"+want)
977977
}
978978

979-
// @intent render a Wiki tree node as fallback Markdown when no generated file exists.
979+
// @intent render a Wiki tree node as generated-doc-shaped fallback Markdown when no generated file exists.
980980
func nodeMarkdown(node *ragindex.TreeNode) string {
981-
parts := []string{fmt.Sprintf("# %s", node.Label)}
982-
if strings.TrimSpace(node.Summary) != "" {
983-
parts = append(parts, strings.TrimSpace(node.Summary))
981+
parts := []string{
982+
"<!-- generated-by: code-context-graph docs -->",
983+
fmt.Sprintf("# %s", node.Label),
984+
}
985+
if summary := cleanMarkdownText(node.Summary); summary != "" {
986+
parts = append(parts, "> "+summary)
984987
}
985988
if len(node.Children) > 0 {
986-
var children []string
987-
for _, child := range node.Children {
988-
line := "- " + child.Label
989-
if strings.TrimSpace(child.Summary) != "" {
990-
line += ": " + strings.TrimSpace(child.Summary)
989+
sections := nodeMarkdownSections(node.Children)
990+
for _, section := range []string{"Files", "Packages", "Functions", "Classes", "Types", "Tests", "Symbols", "Children"} {
991+
children := sections[section]
992+
if len(children) == 0 {
993+
continue
991994
}
992-
children = append(children, line)
995+
lines := []string{"## " + section}
996+
for _, child := range children {
997+
lines = append(lines, nodeMarkdownChild(child)...)
998+
}
999+
parts = append(parts, strings.Join(lines, "\n"))
9931000
}
994-
parts = append(parts, strings.Join(children, "\n"))
9951001
}
9961002
return strings.Join(parts, "\n\n")
9971003
}
9981004

1005+
// @intent group fallback child nodes into stable sections that the Wiki visual renderer can cardify.
1006+
func nodeMarkdownSections(children []*ragindex.TreeNode) map[string][]*ragindex.TreeNode {
1007+
sections := map[string][]*ragindex.TreeNode{}
1008+
for _, child := range children {
1009+
section := nodeMarkdownSection(child)
1010+
sections[section] = append(sections[section], child)
1011+
}
1012+
return sections
1013+
}
1014+
1015+
// @intent map graph node kinds to generated docs section names for DB-backed Wiki fallback.
1016+
func nodeMarkdownSection(node *ragindex.TreeNode) string {
1017+
switch node.Kind {
1018+
case "file":
1019+
return "Files"
1020+
case "package":
1021+
return "Packages"
1022+
case string(model.NodeKindFunction):
1023+
return "Functions"
1024+
case string(model.NodeKindClass):
1025+
return "Classes"
1026+
case string(model.NodeKindType):
1027+
return "Types"
1028+
case string(model.NodeKindTest):
1029+
return "Tests"
1030+
case "symbol":
1031+
return "Symbols"
1032+
default:
1033+
return "Children"
1034+
}
1035+
}
1036+
1037+
// @intent render one fallback tree child in the same symbol-card Markdown shape as generated docs.
1038+
func nodeMarkdownChild(child *ragindex.TreeNode) []string {
1039+
lines := []string{"", "### " + child.Label, ""}
1040+
description := cleanMarkdownText(child.Summary)
1041+
if child.Details != nil && child.Details.Annotation != nil {
1042+
if summary := cleanMarkdownText(child.Details.Annotation.Summary); summary != "" {
1043+
description = summary
1044+
}
1045+
}
1046+
if description != "" {
1047+
lines = append(lines, description, "")
1048+
}
1049+
if child.Details != nil {
1050+
if lineRange := markdownLineRange(child.Details.StartLine, child.Details.EndLine); lineRange != "" {
1051+
lines = append(lines, "- **Lines:** "+lineRange)
1052+
}
1053+
if filePath := cleanMarkdownText(child.Details.FilePath); filePath != "" {
1054+
lines = append(lines, "- **File:** "+filePath)
1055+
}
1056+
if language := cleanMarkdownText(child.Details.Language); language != "" {
1057+
lines = append(lines, "- **Language:** "+language)
1058+
}
1059+
if child.Details.Annotation != nil {
1060+
lines = append(lines, annotationMarkdownBlocks(child.Details.Annotation)...)
1061+
}
1062+
return lines
1063+
}
1064+
if child.Kind != "" {
1065+
lines = append(lines, "- **Kind:** "+child.Kind)
1066+
}
1067+
if child.DocPath != "" {
1068+
lines = append(lines, "- **Path:** "+child.DocPath)
1069+
}
1070+
return lines
1071+
}
1072+
1073+
// @intent format annotation tags into labels already understood by the Wiki generated-doc renderer.
1074+
func annotationMarkdownBlocks(annotation *ragindex.AnnotationDetail) []string {
1075+
blocks := []annotationMarkdownBlock{}
1076+
index := map[string]int{}
1077+
add := func(label, value string) {
1078+
value = cleanMarkdownText(value)
1079+
if value == "" {
1080+
return
1081+
}
1082+
pos, ok := index[label]
1083+
if !ok {
1084+
pos = len(blocks)
1085+
index[label] = pos
1086+
blocks = append(blocks, annotationMarkdownBlock{label: label})
1087+
}
1088+
blocks[pos].items = append(blocks[pos].items, value)
1089+
}
1090+
for _, tag := range annotation.Tags {
1091+
value := annotationTagMarkdownValue(tag)
1092+
switch tag.Kind {
1093+
case model.TagIntent:
1094+
add("Intent", value)
1095+
case model.TagDomainRule:
1096+
add("Domain Rules", value)
1097+
case model.TagSideEffect:
1098+
add("Side Effects", value)
1099+
case model.TagMutates:
1100+
add("Mutates", value)
1101+
case model.TagRequires:
1102+
add("Requires", value)
1103+
case model.TagEnsures:
1104+
add("Ensures", value)
1105+
case model.TagParam:
1106+
add("Params", formatParamMarkdownTag(tag))
1107+
case model.TagReturn:
1108+
add("Returns", value)
1109+
case model.TagSee:
1110+
add("See", value)
1111+
case model.TagThrows:
1112+
add("Throws", value)
1113+
default:
1114+
add(string(tag.Kind), value)
1115+
}
1116+
}
1117+
if context := cleanMarkdownText(annotation.Context); context != "" {
1118+
add("Context", context)
1119+
}
1120+
1121+
var lines []string
1122+
for _, block := range blocks {
1123+
if len(block.items) == 1 && (block.label == "Intent" || block.label == "Returns" || block.label == "See") {
1124+
lines = append(lines, fmt.Sprintf("- **%s:** %s", block.label, block.items[0]))
1125+
continue
1126+
}
1127+
lines = append(lines, fmt.Sprintf("- **%s:**", block.label))
1128+
for _, item := range block.items {
1129+
lines = append(lines, " - "+item)
1130+
}
1131+
}
1132+
return lines
1133+
}
1134+
1135+
// annotationMarkdownBlock groups same-label annotation fallback lines before Markdown rendering.
1136+
// @intent keep annotation Markdown output stable while preserving original tag ordering by first label occurrence.
1137+
type annotationMarkdownBlock struct {
1138+
label string
1139+
items []string
1140+
}
1141+
1142+
// @intent preserve annotation tag name/type context in fallback Markdown without exposing raw JSON.
1143+
func annotationTagMarkdownValue(tag ragindex.DocTagDetail) string {
1144+
parts := []string{}
1145+
if strings.TrimSpace(tag.Name) != "" {
1146+
parts = append(parts, tag.Name)
1147+
}
1148+
if strings.TrimSpace(tag.Value) != "" {
1149+
parts = append(parts, tag.Value)
1150+
}
1151+
return strings.Join(parts, " — ")
1152+
}
1153+
1154+
// @intent format @param tags consistently with browser-side generated doc fallback.
1155+
func formatParamMarkdownTag(tag ragindex.DocTagDetail) string {
1156+
name := strings.TrimSpace(tag.Name)
1157+
if name == "" {
1158+
name = "param"
1159+
}
1160+
if typ := strings.TrimSpace(tag.Type); typ != "" {
1161+
name = fmt.Sprintf("%s [%s]", name, typ)
1162+
}
1163+
if value := cleanMarkdownText(tag.Value); value != "" {
1164+
return name + " — " + value
1165+
}
1166+
return name
1167+
}
1168+
1169+
// @intent keep fallback Markdown attributes single-line so the visual parser can read them predictably.
1170+
func cleanMarkdownText(value string) string {
1171+
return strings.Join(strings.Fields(value), " ")
1172+
}
1173+
1174+
// @intent format graph node source ranges for generated-doc-compatible fallback Markdown.
1175+
func markdownLineRange(start, end int) string {
1176+
if start > 0 && end > 0 {
1177+
return fmt.Sprintf("%d-%d", start, end)
1178+
}
1179+
if start > 0 {
1180+
return strconv.Itoa(start)
1181+
}
1182+
if end > 0 {
1183+
return strconv.Itoa(end)
1184+
}
1185+
return ""
1186+
}
1187+
9991188
// @intent normalize and validate the namespace query parameter shared by Wiki API endpoints.
10001189
func namespaceParam(w http.ResponseWriter, r *http.Request) (string, bool) {
10011190
ns := ctxns.Normalize(strings.TrimSpace(r.URL.Query().Get("namespace")))

internal/wikiserver/server_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,10 @@ func TestAPI_DocFallsBackToDBTreeSummaryWhenWikiIndexMissing(t *testing.T) {
448448
if rec.Code != http.StatusOK {
449449
t.Fatalf("doc fallback status = %d body=%s", rec.Code, rec.Body.String())
450450
}
451-
if !strings.Contains(rec.Body.String(), "payment settlement workflow") || !strings.Contains(rec.Body.String(), `"generated":false`) {
451+
if !strings.Contains(rec.Body.String(), "payment settlement workflow") ||
452+
!strings.Contains(rec.Body.String(), `"generated":false`) ||
453+
!strings.Contains(rec.Body.String(), "generated-by: code-context-graph docs") ||
454+
!strings.Contains(rec.Body.String(), "## Functions") {
452455
t.Fatalf("expected DB summary fallback, got %s", rec.Body.String())
453456
}
454457
}

web/wiki/src/App.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ export default function App() {
163163
}
164164
try {
165165
const res = await getDoc(ns, item.path, token);
166+
if (res.generated === false) {
167+
setDocPath(fallbackDocDisplayPath(item, res.path));
168+
}
166169
setDocContent(res.content);
167170
setNeedsToken(false);
168171
} catch (err) {
@@ -572,6 +575,21 @@ function docDisplayPath(item: SelectedDoc) {
572575
return item.label || "";
573576
}
574577

578+
// @intent show DB-backed synthetic docs as source locations instead of missing generated docs paths.
579+
function fallbackDocDisplayPath(item: SelectedDoc, path: string) {
580+
if (item.details?.file_path) {
581+
return `${item.details.file_path}#${item.details.qualified_name || item.label}`;
582+
}
583+
const sourcePath = sourcePathFromDocPath(path || item.path);
584+
return sourcePath || item.label || docDisplayPath(item);
585+
}
586+
587+
// @intent recover the original source file path from ccg generated docs paths used by tree nodes.
588+
function sourcePathFromDocPath(path: string) {
589+
if (!path.startsWith("docs/") || !path.endsWith(".md")) return "";
590+
return path.slice("docs/".length, -".md".length);
591+
}
592+
575593
// @intent build a local fallback Markdown context when server-side docs are unavailable.
576594
function selectedText(items: SelectedDoc[]) {
577595
return items.map(selectedItemMarkdown).join("\n\n");

web/wiki/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export type DocResponse = {
118118
path: string;
119119
resolved: string;
120120
content: string;
121+
generated?: boolean;
121122
};
122123

123124
// @intent describe the Wiki and graph destination resolved from one ccg:// ref.

0 commit comments

Comments
 (0)