chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
// Step is one node in a discovered execution flow. Depth preserves the
|
||||
// call-tree shape so the UI can render branches instead of flattening
|
||||
// siblings into a false sequence: traceForward emits DFS preorder, and
|
||||
// the parent of a step is the nearest preceding step with a smaller
|
||||
// depth. Sibling order in the slice is the child-declaration order of
|
||||
// the parent function.
|
||||
type Step struct {
|
||||
ID string `json:"id"`
|
||||
Depth int `json:"depth"`
|
||||
}
|
||||
|
||||
// Process represents a discovered execution flow in the codebase.
|
||||
type Process struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"` // human-readable name
|
||||
EntryPoint string `json:"entry_point"` // node ID of the entry function
|
||||
Steps []Step `json:"steps"` // DFS preorder with call-tree depth
|
||||
StepCount int `json:"step_count"`
|
||||
Files []string `json:"files"` // unique files touched
|
||||
Score float64 `json:"score"` // entry point confidence score
|
||||
}
|
||||
|
||||
// ProcessResult is the output of process discovery.
|
||||
type ProcessResult struct {
|
||||
Processes []Process `json:"processes"`
|
||||
NodeToProcs map[string][]string `json:"node_to_processes"` // nodeID → process IDs
|
||||
}
|
||||
|
||||
// DiscoverProcesses finds execution flows by identifying entry points and tracing forward.
|
||||
func DiscoverProcesses(g graph.Store) *ProcessResult {
|
||||
nodes := g.AllNodes()
|
||||
// Meta-less call-edge scan (see LightEdgeScanner): process discovery reads only
|
||||
// endpoints off each call edge.
|
||||
edges := graph.EdgesForKindsLight(g, graph.EdgeCalls)
|
||||
|
||||
// Build call graph adjacency (forward only)
|
||||
callees := make(map[string][]string) // who does this function call?
|
||||
callers := make(map[string][]string) // who calls this function?
|
||||
|
||||
for _, e := range edges {
|
||||
if e.Kind == graph.EdgeCalls {
|
||||
callees[e.From] = append(callees[e.From], e.To)
|
||||
callers[e.To] = append(callers[e.To], e.From)
|
||||
}
|
||||
}
|
||||
|
||||
// Score each function/method as a potential entry point
|
||||
type scored struct {
|
||||
node *graph.Node
|
||||
score float64
|
||||
}
|
||||
var candidates []scored
|
||||
|
||||
nodeMap := make(map[string]*graph.Node)
|
||||
for _, n := range nodes {
|
||||
nodeMap[n.ID] = n
|
||||
if n.Kind != graph.KindFunction && n.Kind != graph.KindMethod {
|
||||
continue
|
||||
}
|
||||
|
||||
score := scoreEntryPoint(n, len(callees[n.ID]), len(callers[n.ID]))
|
||||
if score > 0.5 {
|
||||
candidates = append(candidates, scored{n, score})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].score > candidates[j].score
|
||||
})
|
||||
|
||||
// Trace forward from each entry point to build processes
|
||||
result := &ProcessResult{
|
||||
NodeToProcs: make(map[string][]string),
|
||||
}
|
||||
|
||||
seen := make(map[string]bool) // avoid duplicate processes
|
||||
for i, c := range candidates {
|
||||
if i >= 50 { // cap at 50 processes
|
||||
break
|
||||
}
|
||||
if seen[c.node.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
steps := traceForward(c.node.ID, callees, 15) // max depth 15
|
||||
if len(steps) < 2 {
|
||||
continue // not interesting
|
||||
}
|
||||
seen[c.node.ID] = true
|
||||
|
||||
fileSet := make(map[string]bool)
|
||||
for _, s := range steps {
|
||||
if n, ok := nodeMap[s.ID]; ok {
|
||||
fileSet[n.FilePath] = true
|
||||
}
|
||||
}
|
||||
files := make([]string, 0, len(fileSet))
|
||||
for f := range fileSet {
|
||||
files = append(files, f)
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
procID := fmt.Sprintf("process-%d", len(result.Processes))
|
||||
proc := Process{
|
||||
ID: procID,
|
||||
Name: inferProcessName(c.node),
|
||||
EntryPoint: c.node.ID,
|
||||
Steps: steps,
|
||||
StepCount: len(steps),
|
||||
Files: files,
|
||||
Score: c.score,
|
||||
}
|
||||
result.Processes = append(result.Processes, proc)
|
||||
|
||||
for _, s := range steps {
|
||||
result.NodeToProcs[s.ID] = append(result.NodeToProcs[s.ID], procID)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func scoreEntryPoint(n *graph.Node, calleeCount, callerCount int) float64 {
|
||||
if calleeCount == 0 {
|
||||
return 0 // leaf functions are not entry points
|
||||
}
|
||||
|
||||
// Base score: ratio of outgoing to incoming calls
|
||||
base := float64(calleeCount) / (float64(callerCount) + 1.0)
|
||||
|
||||
// Name pattern multiplier
|
||||
nameMult := namePatternMultiplier(n.Name, n.Language)
|
||||
|
||||
// Export/visibility multiplier
|
||||
exportMult := 1.0
|
||||
if isExportedForProcess(n) {
|
||||
exportMult = 1.5
|
||||
}
|
||||
|
||||
// Low caller count bonus (true entry points have few callers)
|
||||
callerMult := 1.0
|
||||
if callerCount == 0 {
|
||||
callerMult = 2.0
|
||||
} else if callerCount <= 2 {
|
||||
callerMult = 1.3
|
||||
}
|
||||
|
||||
// Framework entry points stamped by the entrypoints detector (Spring
|
||||
// handlers, JAX-RS resources, annotated servlets, the JVM main, …)
|
||||
// are invoked by a runtime, not application code — the most reliable
|
||||
// process roots. Test fixtures are stamped too (so dead-code keeps
|
||||
// them live) but are noise as top-level processes, so skip the boost
|
||||
// for them.
|
||||
entryMult := 1.0
|
||||
if ep, _ := n.Meta["entry_point"].(bool); ep {
|
||||
if kind, _ := n.Meta["entry_point_kind"].(string); !strings.HasPrefix(kind, "junit:") {
|
||||
entryMult = 2.0
|
||||
}
|
||||
}
|
||||
|
||||
return base * nameMult * exportMult * callerMult * entryMult
|
||||
}
|
||||
|
||||
// isExportedForProcess mirrors the dead-code visibility logic: for
|
||||
// keyword-visibility languages (Java) it trusts the recorded modifier
|
||||
// so a private helper isn't handed the public-API entry-point boost.
|
||||
func isExportedForProcess(n *graph.Node) bool {
|
||||
if n.Language == "java" {
|
||||
if v, ok := n.Meta["visibility"].(string); ok && v != "" {
|
||||
return v == "public" || v == "protected"
|
||||
}
|
||||
}
|
||||
return isExported(n.Name, n.Language)
|
||||
}
|
||||
|
||||
func namePatternMultiplier(name, lang string) float64 {
|
||||
lower := strings.ToLower(name)
|
||||
|
||||
// High-value entry point patterns
|
||||
entryPatterns := []string{
|
||||
"main", "init", "run", "start", "serve", "listen",
|
||||
"handle", "handler", "controller", "middleware",
|
||||
"route", "endpoint", "dispatch",
|
||||
}
|
||||
for _, p := range entryPatterns {
|
||||
if strings.HasPrefix(lower, p) || strings.HasSuffix(lower, p) {
|
||||
return 1.5
|
||||
}
|
||||
}
|
||||
|
||||
// Go-specific
|
||||
if lang == "go" {
|
||||
if strings.HasPrefix(name, "New") || strings.HasPrefix(name, "Serve") {
|
||||
return 1.3
|
||||
}
|
||||
if strings.HasPrefix(name, "Test") || strings.HasPrefix(name, "Benchmark") {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// Utility patterns (deprioritize)
|
||||
utilPatterns := []string{
|
||||
"get", "set", "is", "has", "to", "from", "parse",
|
||||
"format", "validate", "helper", "util", "string",
|
||||
}
|
||||
for _, p := range utilPatterns {
|
||||
if strings.HasPrefix(lower, p) {
|
||||
return 0.5
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0
|
||||
}
|
||||
|
||||
func isExported(name, lang string) bool {
|
||||
if lang == "go" {
|
||||
return len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z'
|
||||
}
|
||||
// For other languages, assume exported if not starting with underscore
|
||||
return !strings.HasPrefix(name, "_")
|
||||
}
|
||||
|
||||
func traceForward(startID string, callees map[string][]string, maxDepth int) []Step {
|
||||
var result []Step
|
||||
visited := make(map[string]bool)
|
||||
|
||||
var dfs func(id string, depth int)
|
||||
dfs = func(id string, depth int) {
|
||||
if visited[id] || depth > maxDepth {
|
||||
return
|
||||
}
|
||||
visited[id] = true
|
||||
result = append(result, Step{ID: id, Depth: depth})
|
||||
|
||||
for _, callee := range callees[id] {
|
||||
if !visited[callee] {
|
||||
dfs(callee, depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dfs(startID, 0)
|
||||
return result
|
||||
}
|
||||
|
||||
func inferProcessName(n *graph.Node) string {
|
||||
name := n.Name
|
||||
lower := strings.ToLower(name)
|
||||
|
||||
// Try to extract a descriptive name
|
||||
if lower == "main" {
|
||||
return "main execution"
|
||||
}
|
||||
if strings.HasPrefix(lower, "handle") {
|
||||
subject := strings.TrimPrefix(name, "Handle")
|
||||
subject = strings.TrimPrefix(subject, "handle")
|
||||
if subject != "" {
|
||||
return strings.ToLower(subject[:1]) + subject[1:] + " handling"
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(lower, "serve") {
|
||||
return name + " flow"
|
||||
}
|
||||
if strings.HasPrefix(name, "New") {
|
||||
return strings.TrimPrefix(name, "New") + " initialization"
|
||||
}
|
||||
if strings.HasPrefix(name, "Test") {
|
||||
return name
|
||||
}
|
||||
|
||||
return name + " flow"
|
||||
}
|
||||
Reference in New Issue
Block a user