package beads import "strings" // depTreeMaxDepth / depTreeMaxNodes bound the transitive walk so a dense or // cyclic graph can never blow up a detail page. const ( depTreeMaxDepth = 6 depTreeMaxNodes = 200 ) // depLink is one outgoing edge in a dependency adjacency map: the neighbor id // and the edge's dependency type. type depLink struct { to string typ string } // buildDepTree walks the adjacency from root (exclusive) breadth-consistent // pre-order, flattening the reachable set into indented nodes. Each issue // appears once (first path wins); depth and node count are bounded so a dense // or cyclic graph is safe. func buildDepTree(root string, adj map[string][]depLink, titleOf, statusOf, catByStatus map[string]string) []TreeNode { var out []TreeNode visited := map[string]bool{root: true} var dfs func(id string, depth int) dfs = func(id string, depth int) { if depth > depTreeMaxDepth || len(out) >= depTreeMaxNodes { return } for _, lnk := range adj[id] { if visited[lnk.to] || len(out) >= depTreeMaxNodes { continue } visited[lnk.to] = true st := statusOf[lnk.to] out = append(out, TreeNode{ ID: lnk.to, Title: titleOf[lnk.to], Type: lnk.typ, Status: st, Closed: statusCategory(st, catByStatus) == "closed", Depth: depth, }) dfs(lnk.to, depth+1) } } dfs(root, 0) return out } // hasTransitive reports whether a flattened tree reaches past the direct edges // (any Depth>0 node) — the signal that it adds something the flat list doesn't. func hasTransitive(nodes []TreeNode) bool { for _, n := range nodes { if n.Depth > 0 { return true } } return false } // depActivity synthesizes a History entry for a dependency edge touching `want`. // beads emits no audit event when a link is added, but the dependencies row // carries created_at/created_by, so edge additions — most usefully subtasks // linked under an epic — still appear on the timeline. Returns ok=false when the // edge does not touch `want` or the row has no timestamp (older schema without // created_at: skip rather than emit a blank-dated entry). func depActivity(want, from, to, typ, at, by string) (Activity, bool) { if at == "" || (from != want && to != want) { return Activity{}, false } var summary string switch strings.ToLower(strings.TrimSpace(typ)) { case "parent-child": if to == want { summary = "added subtask " + from // want is the epic/parent } else { summary = "added under epic " + to // want is the child } case "blocks": if from == want { summary = "added dependency on " + to } else { summary = from + " now depends on this" } case "related": // Related is symmetric; emit once (from the issue_id side) to avoid a // duplicate entry on both endpoints. if from != want { return Activity{}, false } summary = "linked " + to + " (related)" default: if from == want { summary = "added " + typ + " dependency on " + to } else { return Activity{}, false } } return Activity{Kind: "dep", Event: "dependency", Actor: by, Summary: summary, CreatedAt: at}, true }