~bigbes/sr-ht-dolt

ref: 4038ec2b4469113613ecf777e40a097bd8f09da2 sr-ht-dolt/beads/stream.go -rw-r--r-- 5.1 KiB
4038ec2b — Eugene Blikh mcpsrv: answer what is ready across every tracker 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
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
150
151
152
153
154
155
156
157
package beads

import (
	"sort"
	"strings"
)

// --- the stream layout -------------------------------------------------------
//
// The board is four lanes side by side and sorts every one of them the same way,
// which is right for comparing lanes. A single column is read top to bottom, and
// then each section answers a different question, so each gets its own order.

// parseLayout reads ?layout=. Anything other than "stream" — absent, misspelled,
// or a layout that no longer exists — is the board: a stale link renders the
// default page rather than an error.
func parseLayout(v string) string {
	if strings.EqualFold(strings.TrimSpace(v), LayoutStream) {
		return LayoutStream
	}
	return LayoutBoard
}

// streamSections turns the finished lanes into the stream's sections, in parade
// order. Bucketing is not repeated: each section is the lane it names, so one
// issue lands in exactly one section and the section counts are the marquee's
// counts by construction. Only the order inside a section changes, on a copy —
// the lanes keep the board order the board renders.
//
// created is the id → created_at map the board sort already built; Lined Up is
// the one section that orders by it.
func streamSections(lanes []Lane, created map[string]string) []Section {
	sections := make([]Section, 0, len(lanes))
	for _, lane := range lanes {
		sec := Section{Lane: lane}
		sec.Issues = append([]Card(nil), lane.Issues...)
		// The lane set is built in Build and this switch covers it; a lane outside
		// it would keep the board's order and carry no note, which is the honest
		// rendering of "this section has no reading order of its own".
		switch lane.Slug {
		case "rolling":
			sortRolling(sec.Issues)
			sec.Note = "most recently started first"
		case "lined-up":
			sortLinedUp(sec.Issues, created)
			sec.Note = "ready first, then priority"
		case "stalled":
			sortStalled(sec.Issues)
			sec.Note = "fewest blockers first"
		case "past-stand":
			sortPastStand(sec.Issues)
			sec.Note = "closed, newest first"
			// The largest section and the least actionable one: it opens closed so
			// the three sections above it stay reachable without scrolling past a
			// log. <details> does that with no JavaScript.
			sec.Collapsed = true
		}
		sections = append(sections, sec)
	}
	return sections
}

// sortRolling orders in-progress work by when it was picked up, most recent
// first — what was started last is what is actually being worked on. Ties fall
// back to priority, then id.
func sortRolling(cards []Card) {
	sort.SliceStable(cards, func(i, j int) bool {
		if c := cmpTimeDesc(cards[i].StartedAt, cards[j].StartedAt); c != 0 {
			return c < 0
		}
		if pi, pj := priorityRank(cards[i].Priority), priorityRank(cards[j].Priority); pi != pj {
			return pi < pj
		}
		return cards[i].ID < cards[j].ID
	})
}

// sortLinedUp leads with the ready set: this is the "what can I take" section,
// and an issue that is actionable now belongs above one that is merely open.
// Then priority, then oldest first, then id.
func sortLinedUp(cards []Card, created map[string]string) {
	sort.SliceStable(cards, func(i, j int) bool {
		if ri, rj := cards[i].Ready, cards[j].Ready; ri != rj {
			return ri
		}
		if pi, pj := priorityRank(cards[i].Priority), priorityRank(cards[j].Priority); pi != pj {
			return pi < pj
		}
		if c := cmpTimeAsc(created[cards[i].ID], created[cards[j].ID]); c != 0 {
			return c < 0
		}
		return cards[i].ID < cards[j].ID
	})
}

// sortStalled orders by how far each issue is from moving: one blocker away is
// nearer than five. Then priority, then id.
func sortStalled(cards []Card) {
	sort.SliceStable(cards, func(i, j int) bool {
		if bi, bj := cards[i].BlockedBy, cards[j].BlockedBy; bi != bj {
			return bi < bj
		}
		if pi, pj := priorityRank(cards[i].Priority), priorityRank(cards[j].Priority); pi != pj {
			return pi < pj
		}
		return cards[i].ID < cards[j].ID
	})
}

// sortPastStand orders the log by when work finished, most recent on top. There
// is no priority tiebreak: nothing here is prioritised any more.
func sortPastStand(cards []Card) {
	sort.SliceStable(cards, func(i, j int) bool {
		if c := cmpTimeDesc(cards[i].ClosedAt, cards[j].ClosedAt); c != 0 {
			return c < 0
		}
		return cards[i].ID < cards[j].ID
	})
}

// cmpTimeAsc compares two stored timestamps oldest first, with the unset value
// LAST rather than first. beads timestamps share the "YYYY-MM-DD HH:MM:SS"
// shape, so a lexical compare is a time compare — but "" is lexically smaller
// than every date, and an issue whose start or close was never recorded is not
// the oldest issue in the section. It is the one nothing is known about, and it
// belongs at the bottom.
func cmpTimeAsc(a, b string) int {
	switch {
	case a == b:
		return 0
	case a == "":
		return 1
	case b == "":
		return -1
	case a < b:
		return -1
	default:
		return 1
	}
}

// cmpTimeDesc is cmpTimeAsc reversed for the present values, keeping the unset
// one last (a plain negation would float it to the top).
func cmpTimeDesc(a, b string) int {
	switch {
	case a == b:
		return 0
	case a == "":
		return 1
	case b == "":
		return -1
	case a > b:
		return -1
	default:
		return 1
	}
}