~bigbes/sr-ht-dolt

ref: a41949952f01c47f58cc35fbba02205afd713d41 sr-ht-dolt/beads/deps.go -rw-r--r-- 3.0 KiB
a4194995 — Eugene Blikh mcpsrv: report the projection's clip on get_issue and list_milestones 5 days ago
                                                                                
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
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
}