Files
zzet--gortex/internal/mcp/tools_analyze_edge_audit.go
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

188 lines
5.5 KiB
Go

// edge_audit is a self-diagnostic analyzer: rather than answering a
// question about the code, it reports where the *graph itself* is
// likely incomplete. Missing edge types are the root cause of
// dead-code false positives — a symbol looks unreachable only because
// the edge that reaches it was never extracted — so this view grades
// call-graph resolution confidence and surfaces the symbols most at
// risk of a wrong "dead" verdict.
package mcp
import (
"context"
"fmt"
"sort"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"github.com/zzet/gortex/internal/graph"
)
// edgeTierLabel buckets an edge Origin into a coarse confidence tier.
func edgeTierLabel(origin string) string {
switch origin {
case graph.OriginLSPResolved, graph.OriginLSPDispatch:
return "lsp"
case graph.OriginASTResolved:
return "ast_resolved"
case graph.OriginASTInferred:
return "ast_inferred"
case graph.OriginTextMatched:
return "text_matched"
default:
return "unknown"
}
}
// handleAnalyzeEdgeAudit grades graph completeness. It reports the
// distribution of edges (and call edges specifically) across
// resolution-confidence tiers, plus the symbols most likely to be
// dead-code false positives: interfaces with no implementor, targets
// reached only from test code, and call edges resolved by the weakest
// text-matching tier.
func (s *Server) handleAnalyzeEdgeAudit(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
sample := 10
if v, ok := args["limit"].(float64); ok && v > 0 {
sample = int(v)
}
edgeTiers := map[string]int{}
callTiers := map[string]int{}
inCalls := map[string][]string{} // target → caller IDs
implemented := map[string]bool{} // interface ID → has an implementor
var weakCalls []string // text-matched "from -> to"
// When the request narrows scope (workspace-bound session or repo
// allow-set), drop edges/nodes outside it so every count map and
// diagnostic array recomputes from the in-scope subgraph. Unbound
// requests skip the gate entirely — a byte-for-byte no-op.
scoped := s.scopeFiltersActive(ctx)
for _, e := range s.graph.AllEdges() {
if scoped && (!s.analyzeNodeVisible(ctx, s.graph.GetNode(e.From)) || !s.analyzeNodeVisible(ctx, s.graph.GetNode(e.To))) {
continue
}
tier := edgeTierLabel(e.Origin)
edgeTiers[tier]++
switch e.Kind {
case graph.EdgeCalls:
callTiers[tier]++
inCalls[e.To] = append(inCalls[e.To], e.From)
if e.Origin == graph.OriginTextMatched {
weakCalls = append(weakCalls, e.From+" -> "+e.To)
}
case graph.EdgeImplements:
implemented[e.To] = true
}
}
// Interfaces with no implementor.
var unimplemented []string
// Targets reached only from test symbols — dead-code false
// positives once test callers are policy-excluded.
var testOnly []string
for _, n := range s.graph.AllNodes() {
if scoped && !s.analyzeNodeVisible(ctx, n) {
continue
}
switch n.Kind {
case graph.KindInterface:
if !implemented[n.ID] {
unimplemented = append(unimplemented, n.ID)
}
case graph.KindFunction, graph.KindMethod:
callers := inCalls[n.ID]
if len(callers) == 0 {
continue // uncalled, but not "test-only" — out of scope here
}
allTest := true
for _, c := range callers {
cn := s.graph.GetNode(c)
if cn == nil || !auditIsTestNode(cn) {
allTest = false
break
}
}
if allTest {
testOnly = append(testOnly, n.ID)
}
}
}
sort.Strings(unimplemented)
sort.Strings(testOnly)
sort.Strings(weakCalls)
totalEdges := 0
for _, c := range edgeTiers {
totalEdges += c
}
totalCalls := 0
for _, c := range callTiers {
totalCalls += c
}
highConf := callTiers["lsp"] + callTiers["ast_resolved"]
highPct := 0.0
if totalCalls > 0 {
highPct = float64(highConf) * 100 / float64(totalCalls)
}
payload := map[string]any{
"edge_tiers": edgeTiers,
"call_tiers": callTiers,
"summary": map[string]any{
"total_edges": totalEdges,
"total_call_edges": totalCalls,
"high_confidence_call_pct": round1(highPct),
},
"unimplemented_interfaces": auditBucket(unimplemented, sample),
"test_only_targets": auditBucket(testOnly, sample),
"weak_call_edges": auditBucket(weakCalls, sample),
}
if s.isGCX(ctx, req) {
return s.gcxResponseWithBudget(req)(encodeAnalyze("edge_audit", []map[string]any{payload}))
}
if isCompact(req) {
var b strings.Builder
fmt.Fprintf(&b, "edges=%d calls=%d high_conf=%.1f%%\n", totalEdges, totalCalls, highPct)
fmt.Fprintf(&b, "unimplemented_interfaces=%d test_only_targets=%d weak_call_edges=%d\n",
len(unimplemented), len(testOnly), len(weakCalls))
return mcp.NewToolResultText(b.String()), nil
}
return s.respondJSONOrTOON(ctx, req, payload)
}
// auditBucket packs a count plus a capped, ordered sample.
func auditBucket(ids []string, sample int) map[string]any {
out := map[string]any{"count": len(ids)}
if len(ids) > sample {
out["sample"] = ids[:sample]
out["truncated"] = true
} else if len(ids) > 0 {
out["sample"] = ids
}
return out
}
// auditIsTestNode reports whether n is test code — by the Meta flags
// the test-edge pass stamps, or (via isTestNode) by its file path.
func auditIsTestNode(n *graph.Node) bool {
if n == nil {
return false
}
if n.Meta != nil {
if v, _ := n.Meta["is_test"].(bool); v {
return true
}
if v, _ := n.Meta["is_test_file"].(bool); v {
return true
}
}
return isTestNode(n)
}
// round1 rounds to one decimal place.
func round1(f float64) float64 {
return float64(int(f*10+0.5)) / 10
}