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

161 lines
5.7 KiB
Go

package mcp
// MCP entrypoint for the Temporal dispatch LLM verification pass.
//
// PURPOSE — make resolver.VerifyTemporalEdges reachable from the MCP `analyze`
// surface (the CLI host that originally drove it was removed; analyze is now
// MCP-only). `analyze kind=temporal_verify` runs the precision backstop over
// the active graph's low-confidence Temporal dispatch edges, promoting LLM-
// confirmed edges and suppressing rejected ones, and returns the canonical
// verify report.
// RATIONALE — the verification core (resolver) and its LLM/source adapters
// (analyzer) are injected interfaces; this handler is the only place that wires
// them to the server's live state — the shared LLM provider and the on-disk
// source resolved through the server's own multi-repo/worktree path logic.
// Reusing s.llmService.Provider() avoids constructing a second provider, and
// the source provider delegates to resolveNodePath so paths are correct in
// single-repo, multi-repo, and worktree layouts. With no LLM configured the
// handler returns a clear error instead of running (or panicking).
// KEYWORDS — analyze, temporal, verify, llm, mcp, precision
import (
"context"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/mark3labs/mcp-go/mcp"
"github.com/zzet/gortex/internal/analyzer"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/resolver"
"go.uber.org/zap"
)
// maxTemporalNodeSourceBytes caps the per-node source handed to the LLM so a
// giant function body can't blow the prompt budget. Mirrors the cap baked into
// analyzer.NewFileSourceProvider; restated here because this handler resolves
// paths through the server (resolveNodePath) rather than a bare root join.
const maxTemporalNodeSourceBytes = 6000
// serverSourceProvider implements resolver.TemporalSourceProvider by reading a
// node's source slice from disk via the server's own path resolution
// (resolveNodePath), so the verifier gets correct on-disk source under
// single-repo, multi-repo prefix, and linked-worktree layouts alike. Read file
// bodies are cached for the run.
type serverSourceProvider struct {
s *Server
cache map[string]string // absolute path -> file body ("" = unreadable)
}
func newServerSourceProvider(s *Server) *serverSourceProvider {
return &serverSourceProvider{s: s, cache: map[string]string{}}
}
// NodeSource returns the source text of n's declaration, or ("", false).
func (p *serverSourceProvider) NodeSource(n *graph.Node) (string, bool) {
if n == nil {
return "", false
}
abs, err := p.s.resolveNodePath(n)
if err != nil || abs == "" {
return "", false
}
body, ok := p.fileBody(abs)
if !ok {
return "", false
}
lines := strings.Split(body, "\n")
start, end := n.StartLine, n.EndLine
if start < 1 {
start = 1
}
if start > len(lines) {
return "", false
}
if end < start || end > len(lines) {
end = len(lines)
}
src := strings.Join(lines[start-1:end], "\n")
if len(src) > maxTemporalNodeSourceBytes {
src = src[:maxTemporalNodeSourceBytes] + "\n// …truncated"
}
return src, true
}
func (p *serverSourceProvider) fileBody(abs string) (string, bool) {
if b, ok := p.cache[abs]; ok {
return b, b != ""
}
raw, err := os.ReadFile(abs)
if err != nil {
p.cache[abs] = ""
return "", false
}
p.cache[abs] = string(raw)
return string(raw), true
}
// handleAnalyzeTemporalVerify runs the LLM cleaning pass over the active
// graph's verifiable Temporal dispatch edges and returns the verify report
// (checked/confirmed/rejected/uncertain/errors/details/totals). It requires a
// configured LLM provider — with none, it returns a clear "LLM not configured"
// error result rather than silently no-op'ing or panicking.
func (s *Server) handleAnalyzeTemporalVerify(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
if s.llmService == nil || !s.llmService.Enabled() {
return mcp.NewToolResultError(
"temporal_verify requires a configured LLM provider — set llm.provider " +
"in .gortex.yaml (or GORTEX_LLM_PROVIDER) with a valid model / API key",
), nil
}
provider := s.llmService.Provider()
if provider == nil {
return mcp.NewToolResultError("temporal_verify: LLM provider unavailable"), nil
}
inner := analyzer.NewLLMTemporalVerifierFromProvider(provider)
// Disk-backed verdict cache so re-runs (e.g. in CI) don't re-pay the LLM.
// The key includes the model and the actual caller / target source, so it
// invalidates automatically when the model or the code changes.
cachePath := ""
roots := s.collectRepoRoots("")
rootKeys := make([]string, 0, len(roots))
for k := range roots {
rootKeys = append(rootKeys, k)
}
sort.Strings(rootKeys)
for _, k := range rootKeys {
if roots[k] != "" {
cachePath = filepath.Join(roots[k], ".gortex", "temporal-verify-cache.json")
break
}
}
verifier := analyzer.NewCachingVerifier(inner, provider.Name(), cachePath)
src := newServerSourceProvider(s)
report := resolver.VerifyTemporalEdges(ctx, s.graph, src, verifier)
if err := verifier.Flush(); err != nil && s.logger != nil {
s.logger.Warn("temporal_verify: verdict cache flush failed", zap.Error(err))
}
out := analyzer.VerifyReportToMap(report)
if isCompact(req) {
var b strings.Builder
b.WriteString("checked: ")
b.WriteString(strconv.Itoa(report.Checked))
b.WriteString(", confirmed: ")
b.WriteString(strconv.Itoa(report.Confirmed))
b.WriteString(", rejected: ")
b.WriteString(strconv.Itoa(report.Rejected))
b.WriteString(", uncertain: ")
b.WriteString(strconv.Itoa(report.Uncertain))
b.WriteString(", errors: ")
b.WriteString(strconv.Itoa(report.Errors))
b.WriteByte('\n')
return mcp.NewToolResultText(b.String()), nil
}
return s.respondJSONOrTOON(ctx, req, out)
}