package claudemd import ( "fmt" "sort" "strings" "github.com/zzet/gortex/internal/graph" "github.com/zzet/gortex/internal/query" ) // Generate produces a CLAUDE.md block describing the indexed codebase and how to use Gortex tools. func Generate(engine *query.Engine, _ int) string { stats := engine.Stats() var b strings.Builder b.WriteString("## Codebase Overview (generated by Gortex)\n\n") // Language breakdown. langs := sortedKeys(stats.ByLanguage) if len(langs) > 0 { primary := langs[0] for _, l := range langs { if stats.ByLanguage[l] > stats.ByLanguage[primary] { primary = l } } langParts := make([]string, 0, len(langs)) for _, l := range langs { if l == primary { langParts = append([]string{fmt.Sprintf("%s (primary)", l)}, langParts...) } else { langParts = append(langParts, l) } } fmt.Fprintf(&b, "- **Languages:** %s\n", strings.Join(langParts, ", ")) } // Entry points (main functions). entryPoints := engine.FindSymbols("main", graph.KindFunction) if len(entryPoints) > 0 { paths := make([]string, 0, len(entryPoints)) for _, ep := range entryPoints { paths = append(paths, fmt.Sprintf("`%s`", ep.FilePath)) } fmt.Fprintf(&b, "- **Entry points:** %s\n", strings.Join(paths, ", ")) } // Key symbols (most referenced). topSymbols := findMostReferenced(engine, 10) if len(topSymbols) > 0 { parts := make([]string, 0, len(topSymbols)) for _, s := range topSymbols { parts = append(parts, fmt.Sprintf("`%s` (%d usages)", s.name, s.count)) } fmt.Fprintf(&b, "- **Most-referenced symbols:** %s\n", strings.Join(parts, ", ")) } fmt.Fprintf(&b, "- **Graph size:** %d nodes, %d edges\n", stats.TotalNodes, stats.TotalEdges) // Kind breakdown. if len(stats.ByKind) > 0 { parts := make([]string, 0, len(stats.ByKind)) for _, k := range sortedKeys(stats.ByKind) { parts = append(parts, fmt.Sprintf("%d %ss", stats.ByKind[k], k)) } fmt.Fprintf(&b, "- **Breakdown:** %s\n", strings.Join(parts, ", ")) } b.WriteString("\n## MANDATORY: Use Gortex MCP tools instead of Read/Grep/Glob\n\n") b.WriteString("Gortex is running as an MCP server. You **MUST** prefer graph queries over file reads on every task in this repo — `search_symbols`, `find_usages`, `get_symbol_source`, `get_editing_context`, `smart_context`, `edit_symbol` / `edit_file` / `rename_symbol` / `batch_edit`. PreToolUse hooks deny `Read` / `Grep` / `Glob` against indexed source; the deny message names the right tool. The full per-tool catalog loads via `tools/list` — not restated here.\n\n") b.WriteString("### Calibration: the graph narrows scope, source confirms behavior\n\n") b.WriteString("The mandate above stands — but graph queries *narrow scope*, they do not *replace reading the implementation*. The graph tells you **where** the logic lives and **what** connects to it; the source tells you **how** it behaves. For the symbol you are about to change or depend on, read its full body with `get_symbol_source` — do not act on a one-line summary alone.\n\n") b.WriteString("Be especially deliberate with **behavior-critical code** — database migrations, retry / fallback / error-recovery paths, compatibility shims, concurrency-sensitive sections, and the tests that pin them. For these, call `get_symbol_source` and read the real implementation; never pass `compress_bodies:true`, which elides exactly the branches that carry the risk. Reserve compressed bodies and graph summaries for breadth (surveying many symbols); use full source for the few you are about to commit to.\n\n") b.WriteString("## Required workflow (every task on this repo)\n\n") b.WriteString("These are not suggestions — run each step at the trigger.\n\n") b.WriteString("1. Confirm the daemon is up with `index_health` (cheap liveness + scope). Call `graph_stats` only when you actually need node/edge counts or `per_repo` orientation — it returns a large payload and can block during warmup.\n") b.WriteString("2. If `total_nodes` is 0, **call** `index_repository` with `\".\"` before anything else.\n") b.WriteString("3. In multi-repo mode, **call** `get_active_project` to check scope; use `set_active_project` to switch.\n") b.WriteString("4. Open a non-trivial task with `smart_context` for orientation. For a single known symbol or file, go straight to `search_symbols` / `get_symbol_source` — don't front-load `smart_context` before every read.\n") b.WriteString("5. Before editing a file, **call** `get_editing_context` on it first.\n") b.WriteString("6. Before changing any function signature, **call** `verify_change` to catch broken callers and interface implementors (cross-repo).\n") b.WriteString("7. For any refactor, **call** `get_edit_plan` then `batch_edit` to apply atomically.\n") b.WriteString("8. Verify with the project's real build/test. Reserve `check_guards` for guard-relevant changes and `get_test_targets` to find the tests covering a substantive change — not mechanically after every edit.\n") return b.String() } type symbolRef struct { name string count int } func findMostReferenced(engine *query.Engine, limit int) []symbolRef { allNodes := engine.AllNodes() relevantKinds := map[graph.EdgeKind]bool{ graph.EdgeCalls: true, graph.EdgeReferences: true, graph.EdgeInstantiates: true, graph.EdgeImplements: true, } var refs []symbolRef for _, n := range allNodes { if n.Kind == graph.KindFile || n.Kind == graph.KindImport { continue } count := 0 for _, e := range engine.GetInEdges(n.ID) { if relevantKinds[e.Kind] { count++ } } if count > 0 { refs = append(refs, symbolRef{name: n.Name, count: count}) } } sort.Slice(refs, func(i, j int) bool { return refs[i].count > refs[j].count }) if len(refs) > limit { refs = refs[:limit] } return refs } func sortedKeys(m map[string]int) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys }