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
188 lines
5.5 KiB
Go
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
|
|
}
|