coverage~bigbes/sr-ht-doltede3b0bbbeads/deps.go

Coverage
66.7% 24/36 statements
Δ
Blob
6074216
Uncovered L29-L30L33L79-L99
1 package beads
2
3 import "strings"
4
5 // depTreeMaxDepth / depTreeMaxNodes bound the transitive walk so a dense or
6 // cyclic graph can never blow up a detail page.
7 const (
8 depTreeMaxDepth = 6
9 depTreeMaxNodes = 200
10 )
11
12 // depLink is one outgoing edge in a dependency adjacency map: the neighbor id
13 // and the edge's dependency type.
14 type depLink struct {
15 to string
16 typ string
17 }
18
19 // buildDepTree walks the adjacency from root (exclusive) breadth-consistent
20 // pre-order, flattening the reachable set into indented nodes. Each issue
21 // appears once (first path wins); depth and node count are bounded so a dense
22 // or cyclic graph is safe.
23 40 func buildDepTree(root string, adj map[string][]depLink, titleOf, statusOf, catByStatus map[string]string) []TreeNode {
24 40 var out []TreeNode
25 40 visited := map[string]bool{root: true}
26 40 var dfs func(id string, depth int)
27 68 dfs = func(id string, depth int) {
28 68 if depth > depTreeMaxDepth || len(out) >= depTreeMaxNodes {
29 0 return
30 0 }
31 68 for _, lnk := range adj[id] {
32 28 if visited[lnk.to] || len(out) >= depTreeMaxNodes {
33 0 continue
34 }
35 28 visited[lnk.to] = true
36 28 st := statusOf[lnk.to]
37 28 out = append(out, TreeNode{
38 28 ID: lnk.to,
39 28 Title: titleOf[lnk.to],
40 28 Type: lnk.typ,
41 28 Status: st,
42 28 Closed: statusCategory(st, catByStatus) == "closed",
43 28 Depth: depth,
44 28 })
45 28 dfs(lnk.to, depth+1)
46 }
47 }
48 40 dfs(root, 0)
49 40 return out
50 }
51
52 // hasTransitive reports whether a flattened tree reaches past the direct edges
53 // (any Depth>0 node) — the signal that it adds something the flat list doesn't.
54 40 func hasTransitive(nodes []TreeNode) bool {
55 40 for _, n := range nodes {
56 28 if n.Depth > 0 {
57 2 return true
58 2 }
59 }
60 38 return false
61 }
62
63 // depActivity synthesizes a History entry for a dependency edge touching `want`.
64 // beads emits no audit event when a link is added, but the dependencies row
65 // carries created_at/created_by, so edge additions — most usefully subtasks
66 // linked under an epic — still appear on the timeline. Returns ok=false when the
67 // edge does not touch `want` or the row has no timestamp (older schema without
68 // created_at: skip rather than emit a blank-dated entry).
69 32 func depActivity(want, from, to, typ, at, by string) (Activity, bool) {
70 32 if at == "" || (from != want && to != want) {
71 20 return Activity{}, false
72 20 }
73 12 var summary string
74 12 switch strings.ToLower(strings.TrimSpace(typ)) {
75 12 case "parent-child":
76 12 if to == want {
77 12 summary = "added subtask " + from // want is the epic/parent
78 12 } else {
79 0 summary = "added under epic " + to // want is the child
80 0 }
81 0 case "blocks":
82 0 if from == want {
83 0 summary = "added dependency on " + to
84 0 } else {
85 0 summary = from + " now depends on this"
86 0 }
87 0 case "related":
88 0 // Related is symmetric; emit once (from the issue_id side) to avoid a
89 0 // duplicate entry on both endpoints.
90 0 if from != want {
91 0 return Activity{}, false
92 0 }
93 0 summary = "linked " + to + " (related)"
94 0 default:
95 0 if from == want {
96 0 summary = "added " + typ + " dependency on " + to
97 0 } else {
98 0 return Activity{}, false
99 0 }
100 }
101 12 return Activity{Kind: "dep", Event: "dependency", Actor: by, Summary: summary, CreatedAt: at}, true
102 }