项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

471 行
15 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package mcp
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/zzet/gortex/internal/callpath"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/eval/quality"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/query"
)
// smartContextSections resolves which in-pack enrichment sections a
// smart_context call should attach. Per-call include_* params override the
// project's smart_context config; every section is off by default.
func (s *Server) smartContextSections(args map[string]any, relPath string) config.SmartContextSections {
cfg := config.SmartContextConfig{}
if s.configManager != nil {
cfg = s.configManager.GetRepoConfig(repoPrefixForPath(s, relPath)).MCP.SmartContext
}
return cfg.Resolve(
boolPtrArg(args, "include_call_paths"),
boolPtrArg(args, "include_flows"),
boolPtrArg(args, "include_confidence"),
)
}
// boolPtrArg returns a *bool: the parsed value when the caller passed the key,
// nil when absent — so an unset flag inherits config rather than forcing false.
func boolPtrArg(args map[string]any, key string) *bool {
if v, set := boolArg(args, key); set {
return &v
}
return nil
}
// attachInPackSections records the opt-in in-pack enrichment sections on the
// assembled pack under result["in_pack"]. Only sections with content are
// written, so the default pack stays untouched; later passes attach the flow
// spine and confidence verdict to the same block.
func (s *Server) attachInPackSections(result map[string]any, sections config.SmartContextSections, symbols []*graph.Node) {
block := map[string]any{}
if sections.CallPaths {
if cp := s.inPackCallPaths(symbols); len(cp) > 0 {
block["call_paths"] = cp
}
}
if sections.Flows {
if fl := s.inPackFlows(symbols); fl != nil {
block["flows"] = fl
}
}
if len(block) > 0 {
result["in_pack"] = block
}
}
// inPackFlows builds the flow section: a forward flow spine from the focus
// symbol (the first pack symbol) and the dynamic-dispatch boundaries that spine
// hits — call sites whose target the static graph cannot resolve, where the
// flow would continue at runtime. Returns nil when there is no multi-node spine
// and no boundary to announce.
func (s *Server) inPackFlows(symbols []*graph.Node) map[string]any {
if s.graph == nil || len(symbols) == 0 || symbols[0] == nil {
return nil
}
budget := s.inPackBudget()
spine, boundaries := s.flowSpine(symbols[0].ID, budget.FlowDepth)
if len(boundaries) > budget.MaxBoundaries {
boundaries = boundaries[:budget.MaxBoundaries]
}
out := map[string]any{}
if len(spine) >= 2 {
out["spine"] = spine
}
if len(boundaries) > 0 {
out["boundaries"] = boundaries
}
if len(out) == 0 {
return nil
}
return out
}
// packRecoveryKinds are the edge kinds the edge-recovery pass restores between
// pack symbols — the structural relations a many-rooted retrieval BFS leaves
// disconnected.
var packRecoveryKinds = map[graph.EdgeKind]bool{
graph.EdgeCalls: true,
graph.EdgeExtends: true,
graph.EdgeImplements: true,
graph.EdgeReferences: true,
graph.EdgeOverrides: true,
}
// recoverPackEdges returns the edges (calls/extends/implements/references/
// overrides) that exist between the pack's symbols — the internal connectivity
// a many-rooted retrieval leaves out. Returns nil when fewer than two symbols
// are in the pack or none are connected.
func (s *Server) recoverPackEdges(symbols []*graph.Node) []map[string]any {
if s.graph == nil || len(symbols) < 2 {
return nil
}
ids := make(map[string]bool, len(symbols))
ordered := make([]string, 0, len(symbols))
for _, n := range symbols {
if n != nil && n.ID != "" && !ids[n.ID] {
ids[n.ID] = true
ordered = append(ordered, n.ID)
}
}
sort.Strings(ordered)
seen := map[string]bool{}
var out []map[string]any
for _, id := range ordered {
for _, e := range s.graph.GetOutEdges(id) {
if e == nil || !packRecoveryKinds[e.Kind] || !ids[e.To] {
continue
}
key := e.From + "\x00" + e.To + "\x00" + string(e.Kind)
if seen[key] {
continue
}
seen[key] = true
out = append(out, map[string]any{"from": e.From, "to": e.To, "kind": string(e.Kind)})
}
}
return out
}
// packHierarchySiblings returns type symbols that share a supertype with a pack
// type — its siblings in the class hierarchy (e.g. a pack's InternalEngine and
// the sibling ReadOnlyEngine that both extend Engine). Already-packed types are
// excluded; the result is capped.
func (s *Server) packHierarchySiblings(symbols []*graph.Node) []map[string]any {
if s.graph == nil {
return nil
}
inPack := map[string]bool{}
for _, n := range symbols {
if n != nil {
inPack[n.ID] = true
}
}
parents := map[string]bool{}
for _, n := range symbols {
if n == nil || (n.Kind != graph.KindType && n.Kind != graph.KindInterface) {
continue
}
for _, e := range s.graph.GetOutEdges(n.ID) {
if e != nil && (e.Kind == graph.EdgeExtends || e.Kind == graph.EdgeImplements) {
parents[e.To] = true
}
}
}
if len(parents) == 0 {
return nil
}
parentIDs := make([]string, 0, len(parents))
for p := range parents {
parentIDs = append(parentIDs, p)
}
sort.Strings(parentIDs)
sibSeen := map[string]bool{}
var out []map[string]any
for _, parent := range parentIDs {
for _, e := range s.graph.GetInEdges(parent) {
if e == nil || (e.Kind != graph.EdgeExtends && e.Kind != graph.EdgeImplements) {
continue
}
sib := e.From
if inPack[sib] || sibSeen[sib] {
continue
}
n := s.graph.GetNode(sib)
if n == nil {
continue
}
sibSeen[sib] = true
out = append(out, map[string]any{"id": sib, "name": n.Name, "kind": string(n.Kind), "parent": parent})
}
}
sort.SliceStable(out, func(i, j int) bool { return str(out[i]["id"]) < str(out[j]["id"]) })
if len(out) > 10 {
out = out[:10]
}
if len(out) == 0 {
return nil
}
return out
}
// addInPackSection records one section on result["in_pack"], creating the block
// if it does not exist yet.
func addInPackSection(result map[string]any, key string, val any) {
inPack, _ := result["in_pack"].(map[string]any)
if inPack == nil {
inPack = map[string]any{}
}
inPack[key] = val
result["in_pack"] = inPack
}
// inPackConfidence builds the retrieval-confidence verdict for a task: it runs a
// ranked search for the task and summarises the candidate-score distribution
// (how sharply the top result beats the rest) into a high/medium/low verdict.
// Returns nil when the search yields nothing.
func (s *Server) inPackConfidence(ctx context.Context, task string) map[string]any {
eng := s.engineFor(ctx)
if eng == nil || task == "" {
return nil
}
cands := eng.SearchSymbolsRanked(task, 10, query.QueryOptions{}, nil)
if len(cands) == 0 {
return nil
}
scores := make([]float64, 0, len(cands))
for _, c := range cands {
if c != nil {
scores = append(scores, c.Score)
}
}
return buildConfidenceVerdict(quality.ConfidenceFromScores(task, scores))
}
// buildConfidenceVerdict reduces a confidence record to the in-pack verdict map.
// Returns nil for an empty record.
func buildConfidenceVerdict(rec quality.ConfidenceRecord) map[string]any {
if rec.K == 0 {
return nil
}
return map[string]any{
"verdict": confidenceVerdict(rec),
"top1": rec.Top1,
"top2": rec.Top2,
"ratio_top1_top2": rec.Ratio12,
"k": rec.K,
"std_dev": rec.StdDev,
}
}
// confidenceVerdict classifies a confidence record: a single candidate is
// "single", a sharp top-1 (≥2× the runner-up) is "high", a modest lead
// "medium", and a flat distribution "low" (the ranker is unsure).
func confidenceVerdict(rec quality.ConfidenceRecord) string {
switch {
case rec.K <= 1:
return "single"
case rec.Ratio12 >= 2.0:
return "high"
case rec.Ratio12 >= 1.25:
return "medium"
default:
return "low"
}
}
// lowConfidenceRetrievalNote returns the always-on compact retrieval note for
// a smart_context pack whose underlying retrieval looks untrustworthy. Unlike
// the opt-in verbose confidence block, this fires by default — but only when
// the signal is strong: a flat ranked-score distribution OR a head symbol
// anchored solely by speculative (best-guess dynamic-dispatch) edges. It is
// suppressed for single-symbol / distinctive-identifier lookups. Returns nil
// when the retrieval looks sound or there is nothing to assess.
func (s *Server) lowConfidenceRetrievalNote(ctx context.Context, task string, syms []*graph.Node) map[string]any {
if strings.TrimSpace(task) == "" || len(syms) == 0 {
return nil
}
eng := s.engineFor(ctx)
if eng == nil {
return nil
}
cands := eng.SearchSymbolsRanked(task, 10, query.QueryOptions{}, nil)
scores := make([]float64, 0, len(cands))
for _, c := range cands {
if c != nil {
scores = append(scores, c.Score)
}
}
rec := quality.ConfidenceFromScores(task, scores)
return retrievalNoteFor(task, rec, s.headAnchorsAllSpeculative(ctx, syms), likelyDirsFromSymbols(syms, 4))
}
// retrievalNoteFor is the pure decision behind lowConfidenceRetrievalNote: it
// turns the task, ranked-confidence record, head-anchor provenance, and
// candidate directories into the compact note (or nil). Kept side-effect-free
// so the trigger logic is unit-testable without a live engine.
func retrievalNoteFor(task string, rec quality.ConfidenceRecord, headSpeculative bool, dirs []string) map[string]any {
// A single distinctive identifier is an exact lookup, not a fuzzy search —
// never hedge it. A single ranked candidate is likewise confident.
if isDistinctiveIdentifierQuery(task) || rec.K <= 1 {
return nil
}
var reason string
switch {
case confidenceVerdict(rec) == "low":
reason = fmt.Sprintf("ranked candidates are tightly clustered (top1/top2 ratio %.2f, std_dev %.3f) — the pack may be off-target",
rec.Ratio12, rec.StdDev)
case headSpeculative:
// Provenance fusion: even a non-flat distribution is untrustworthy
// when the head symbol is reachable only by speculative edges.
reason = "the top result is anchored only by speculative (best-guess dynamic-dispatch) edges — treat the pack as a lead, not a fact"
default:
return nil
}
note := map[string]any{
"verdict": "low",
"reason": reason,
"suggested_tools": []string{"find_usages", "search_text", "find_files"},
}
if len(dirs) > 0 {
note["likely_dirs"] = dirs
}
return note
}
// isDistinctiveIdentifierQuery reports whether the task is a single token with
// the shape of a deliberately-typed code identifier (camelCase / snake_case /
// has digits) — an exact lookup that should never draw a low-confidence hedge.
func isDistinctiveIdentifierQuery(task string) bool {
fields := strings.Fields(task)
return len(fields) == 1 && hasIdentifierShape(fields[0])
}
// headAnchorsAllSpeculative reports whether the top pack symbol's semantic
// in-edges (calls / references / overrides / implements) exist and are ALL
// speculative — i.e. the symbol is reachable only by best-guess dispatch, so
// the retrieval that surfaced it rests on the weakest provenance tier.
// Structural containment edges are ignored; a symbol with no semantic in-edges
// is not treated as speculatively anchored.
func (s *Server) headAnchorsAllSpeculative(ctx context.Context, syms []*graph.Node) bool {
if len(syms) == 0 || syms[0] == nil {
return false
}
eng := s.engineFor(ctx)
if eng == nil {
return false
}
var sawSemantic bool
for _, e := range eng.GetInEdges(syms[0].ID) {
if e == nil {
continue
}
switch e.Kind {
case graph.EdgeCalls, graph.EdgeReferences, graph.EdgeOverrides, graph.EdgeImplements:
sawSemantic = true
if e.Origin != graph.OriginSpeculative {
return false
}
}
}
return sawSemantic
}
// likelyDirsFromSymbols collects, in pack order, the distinct parent
// directories of the pack symbols' files (capped at max) — the dirs an agent
// should grep / list when the retrieval note flags low confidence.
func likelyDirsFromSymbols(syms []*graph.Node, max int) []string {
seen := make(map[string]bool)
dirs := make([]string, 0, max)
for _, sym := range syms {
if sym == nil || sym.FilePath == "" {
continue
}
d := slashDir(sym.FilePath)
if d == "" || seen[d] {
continue
}
seen[d] = true
dirs = append(dirs, d)
if len(dirs) >= max {
break
}
}
return dirs
}
// slashDir returns the parent directory of a slash-or-OS path, slash-normalised
// and portable (filepath.Dir would mis-handle "/"-paths on Windows).
func slashDir(p string) string {
p = filepath.ToSlash(p)
if i := strings.LastIndex(p, "/"); i > 0 {
return p[:i]
}
return ""
}
// flowSpine greedily walks forward from the focus over resolved CALLS/REFERENCES
// edges (smallest target id first, for determinism), returning the chain of
// node ids it traverses and the dynamic-dispatch boundaries — out-edges to
// unresolved targets — encountered along the way.
func (s *Server) flowSpine(focus string, maxDepth int) (spine []string, boundaries []map[string]any) {
visited := map[string]bool{focus: true}
bseen := map[string]bool{}
spine = []string{focus}
cur := focus
for depth := 0; depth < maxDepth; depth++ {
next := ""
for _, e := range s.graph.GetOutEdges(cur) {
if e == nil || (e.Kind != graph.EdgeCalls && e.Kind != graph.EdgeReferences) {
continue
}
if graph.IsUnresolvedTarget(e.To) {
key := e.From + "\x00" + e.To
if !bseen[key] {
bseen[key] = true
boundaries = append(boundaries, map[string]any{
"from": e.From,
"target": graph.UnresolvedName(e.To),
"reason": "dynamic_dispatch",
})
}
continue
}
if visited[e.To] {
continue
}
if next == "" || e.To < next {
next = e.To
}
}
if next == "" {
break
}
visited[next] = true
spine = append(spine, next)
cur = next
}
return spine, boundaries
}
// inPackCallPaths builds the anchored call-paths section: the focus symbol (the
// first pack symbol) is the anchor and the rest are roots, so each row shows how
// another pack symbol reaches the focus over the call graph. Returns nil when
// fewer than two symbols are in the pack or none reach the focus.
func (s *Server) inPackCallPaths(symbols []*graph.Node) []map[string]any {
if s.graph == nil || len(symbols) < 2 {
return nil
}
anchor := symbols[0].ID
roots := make([]string, 0, len(symbols)-1)
for _, n := range symbols[1:] {
if n != nil && n.ID != "" {
roots = append(roots, n.ID)
}
}
anchored := callpath.New(s.graph).PathsToAnchor(roots, anchor, callpath.Options{MaxDepth: 8})
if len(anchored) == 0 {
return nil
}
if limit := s.inPackBudget().MaxCallPaths; len(anchored) > limit {
anchored = anchored[:limit]
}
out := make([]map[string]any, 0, len(anchored))
for _, ap := range anchored {
out = append(out, map[string]any{
"root": ap.Root,
"anchor": anchor,
"length": ap.Path.Length,
"confidence": ap.Path.Confidence,
"nodes": ap.Path.Nodes,
})
}
return out
}