a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
139 lines
5.9 KiB
Go
139 lines
5.9 KiB
Go
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
|
|
}
|