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

171 lines
5.2 KiB
Go

package mcp
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// DynamicBoundary is graph.DynamicBoundary — the {site, form, key, candidate}
// descriptor of a runtime-dispatch site. Aliased here so the detector reads
// naturally; the type lives in graph so query.SubGraph can carry it.
type DynamicBoundary = graph.DynamicBoundary
const (
dispatchFormReflection = "reflection"
dispatchFormComputedMember = "computed_member"
dispatchFormEventBus = "event_bus"
)
var (
// reflection / getattr / MethodByName / getMethod — name-driven invocation.
dynReflectionRe = regexp.MustCompile(`(?:getattr\s*\(\s*\w+\s*,\s*|\.MethodByName\s*\(\s*|\.[gG]etMethod\s*\(\s*)['"]?([\w.$]+)`)
// computed-member call: handlers[key](...) / obj[expr](...).
dynComputedMemberRe = regexp.MustCompile(`\w+\s*\[\s*['"]?([\w.$]+)['"]?\s*\]\s*\(`)
// typed/event bus: .emit('x') / .dispatch("x") / .publish(Topic).
dynEventBusRe = regexp.MustCompile(`\.\s*(?:emit|dispatch|publish|sendEvent)\s*\(\s*['"]?([\w.$\-/]+)`)
)
// detectDynamicBoundaries scans a symbol body for runtime-dispatch forms and
// returns one boundary per site. The body is line-comment-stripped first so a
// dispatch-shaped token in a comment doesn't register. resolveCandidates
// supplies the candidate shortlist for a (form, key) — graph-backed in
// production, a stub in tests; nil yields empty shortlists.
func detectDynamicBoundaries(body, filePath string, startLine int, resolveCandidates func(form, key string) []string) []DynamicBoundary {
lines := strings.Split(body, "\n")
var out []DynamicBoundary
seen := map[string]bool{}
emit := func(form, key string, lineIdx int) {
key = strings.TrimSpace(strings.Trim(key, `'"`))
if key == "" {
return
}
site := fmt.Sprintf("%s:%d", filePath, startLine+lineIdx)
dedupe := form + "\x00" + key + "\x00" + site
if seen[dedupe] {
return
}
seen[dedupe] = true
var cands []string
if resolveCandidates != nil {
cands = resolveCandidates(form, key)
}
out = append(out, DynamicBoundary{
Site: site, Form: form, Key: key,
Candidates: cands, AgentNamed: anyAgentNamed(cands),
})
}
for i, raw := range lines {
line := stripLineComment(raw)
for _, m := range dynReflectionRe.FindAllStringSubmatch(line, -1) {
emit(dispatchFormReflection, m[1], i)
}
for _, m := range dynComputedMemberRe.FindAllStringSubmatch(line, -1) {
emit(dispatchFormComputedMember, m[1], i)
}
for _, m := range dynEventBusRe.FindAllStringSubmatch(line, -1) {
emit(dispatchFormEventBus, m[1], i)
}
}
return out
}
// stripLineComment blanks a `//` or `#` line comment (best-effort: a `#` or
// `//` inside a string is rare in dispatch lines and harmless to blank).
func stripLineComment(line string) string {
if i := strings.Index(line, "//"); i >= 0 {
line = line[:i]
}
if i := strings.IndexByte(line, '#'); i >= 0 {
line = line[:i]
}
return line
}
// agentNameRe flags candidate symbols whose name denotes an agent / handler /
// worker — the dispatch targets an agent-style component worth surfacing.
var agentNameRe = regexp.MustCompile(`(?i)(agent|handler|worker|processor|consumer|listener|subscriber)s?$`)
func anyAgentNamed(candidates []string) bool {
for _, c := range candidates {
name := c
if i := strings.LastIndexAny(name, ":./"); i >= 0 {
name = name[i+1:]
}
if agentNameRe.MatchString(name) {
return true
}
}
return false
}
// dynamicBoundariesForSymbol reads a symbol's source and detects the runtime
// dispatch boundaries inside it, resolving candidate targets through the graph.
// Returns nil when the source can't be read or no dispatch is found.
func (s *Server) dynamicBoundariesForSymbol(node *graph.Node) []DynamicBoundary {
if node == nil || node.StartLine <= 0 {
return nil
}
absPath, err := s.resolveNodePath(node)
if err != nil {
return nil
}
content, err := os.ReadFile(absPath) //nolint:gosec // path resolved from the indexed graph
if err != nil {
return nil
}
lines := strings.Split(string(content), "\n")
start := node.StartLine - 1
end := node.EndLine
if end <= 0 || end > len(lines) {
end = len(lines)
}
if start < 0 || start >= end {
return nil
}
body := strings.Join(lines[start:end], "\n")
return detectDynamicBoundaries(body, node.FilePath, node.StartLine, s.dispatchCandidates)
}
// dispatchCandidates is the graph-backed candidate resolver: when the dispatch
// key looks like a symbol name, the shortlist is the functions/methods that
// bear that name (capped). A non-identifier key (a runtime variable) yields no
// shortlist — honest about what's statically knowable.
func (s *Server) dispatchCandidates(_, key string) []string {
if !isIdentifierKey(key) {
return nil
}
var out []string
for _, n := range s.graph.FindNodesByName(key) {
if n == nil {
continue
}
if n.Kind == graph.KindFunction || n.Kind == graph.KindMethod {
out = append(out, n.ID)
if len(out) >= 8 {
break
}
}
}
return out
}
func isIdentifierKey(key string) bool {
if key == "" {
return false
}
for i := 0; i < len(key); i++ {
b := key[i]
if b == '_' || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (i > 0 && b >= '0' && b <= '9') {
continue
}
return false
}
return true
}