-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflows.go
More file actions
149 lines (133 loc) · 4.54 KB
/
Copy pathflows.go
File metadata and controls
149 lines (133 loc) · 4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// @index Flow tracing engine that walks call edges into reusable flow records.
package flows
import (
"context"
"fmt"
"github.com/tae2089/code-context-graph/internal/ctxns"
"github.com/tae2089/code-context-graph/internal/model"
)
// EdgeReader exposes graph traversal primitives needed for flow tracing.
// @intent abstract graph reads so flow tracing can follow call edges from any store
type EdgeReader interface {
GetEdgesFrom(ctx context.Context, nodeID uint) ([]model.Edge, error)
GetNodeByID(ctx context.Context, id uint) (*model.Node, error)
}
// Tracer builds call-chain flows from a starting node.
// @intent produce reusable flow records that describe reachable call paths
type Tracer struct {
store EdgeReader
}
// TraceOptions bounds how far a single flow trace is allowed to expand.
// @intent let callers cap traversal cost when tracing large call graphs
type TraceOptions struct {
MaxNodes int
IncludeFallbackCalls *bool
}
// TraceResult wraps a built flow with the metadata needed to detect truncation.
// @intent communicate truncation status alongside the produced flow
type TraceResult struct {
Flow *model.Flow
Truncated bool
MaxNodes int
ReturnedNodes int
// ContainsFallbackCalls indicates whether the traced flow used at least one fallback call edge.
ContainsFallbackCalls bool
// FallbackEdgesCount counts fallback call edges that participated in flow expansion.
FallbackEdgesCount int
}
// @intent default flow tracing to include fallback call edges unless a caller explicitly requests strict mode.
func defaultTraceOptions(opts TraceOptions) TraceOptions {
if opts.IncludeFallbackCalls != nil {
return opts
}
includeFallbackCalls := true
opts.IncludeFallbackCalls = &includeFallbackCalls
return opts
}
// New creates a flow tracer.
// @intent construct a tracer bound to a graph edge reader
func New(store EdgeReader) *Tracer {
return &Tracer{store: store}
}
// TraceFlow traverses outgoing call edges from a start node.
// @intent capture the reachable call chain from one entry node as a flow
// @param startNodeID graph node where traversal begins
// @return flow containing visited nodes in traversal order
// @domainRule only calls edges expand the traced flow
// @ensures each reachable node is included at most once
func (t *Tracer) TraceFlow(ctx context.Context, startNodeID uint) (*model.Flow, error) {
result, err := t.TraceFlowBounded(ctx, startNodeID, TraceOptions{})
if err != nil {
return nil, err
}
return result.Flow, nil
}
// TraceFlowBounded performs the BFS traversal that backs TraceFlow with explicit limits.
// @intent expose a flow trace variant that can stop early when MaxNodes is reached
// @param startNodeID graph node where traversal begins
// @param opts traversal limits applied during BFS
// @return TraceResult with the built flow and truncation metadata
// @domainRule only calls edges enqueue new BFS nodes
// @ensures Truncated is true only when MaxNodes stopped traversal
func (t *Tracer) TraceFlowBounded(ctx context.Context, startNodeID uint, opts TraceOptions) (*TraceResult, error) {
opts = defaultTraceOptions(opts)
visited := map[uint]bool{}
var members []model.FlowMembership
ordinal := 0
ns := ctxns.FromContext(ctx)
truncated := false
fallbackEdges := 0
containsFallbackCalls := false
queue := []uint{startNodeID}
visited[startNodeID] = true
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
members = append(members, model.FlowMembership{
Namespace: ns,
NodeID: current,
Ordinal: ordinal,
})
ordinal++
edges, err := t.store.GetEdgesFrom(ctx, current)
if err != nil {
return nil, err
}
for _, e := range edges {
if !model.IsCallKind(e.Kind) || visited[e.ToNodeID] {
continue
}
if e.Kind == model.EdgeKindFallbackCalls {
if !*opts.IncludeFallbackCalls {
continue
}
fallbackEdges++
containsFallbackCalls = true
}
if opts.MaxNodes > 0 && len(visited) >= opts.MaxNodes {
truncated = true
break
}
visited[e.ToNodeID] = true
queue = append(queue, e.ToNodeID)
}
}
node, _ := t.store.GetNodeByID(ctx, startNodeID)
name := fmt.Sprintf("flow_from_%d", startNodeID)
if node != nil {
name = fmt.Sprintf("flow_from_%s", node.QualifiedName)
}
flow := &model.Flow{
Namespace: ns,
Name: name,
Members: members,
}
return &TraceResult{
Flow: flow,
Truncated: truncated,
MaxNodes: opts.MaxNodes,
ReturnedNodes: len(members),
ContainsFallbackCalls: containsFallbackCalls,
FallbackEdgesCount: fallbackEdges,
}, nil
}