Files
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 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}