~bigbes/sr-ht-dolt

ref: b6a015582614b2a0ec7d07984db1ce7e8e9f8523 sr-ht-dolt/beads/rows.go -rw-r--r-- 5.7 KiB
b6a01558 — Eugene Blikh web: drop the author from a memory's revision line 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package beads

import (
	"context"
	"errors"
	"sort"
	"strings"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
)

// --- helpers -----------------------------------------------------------------

// readRows reads up to Max rows of a required table and its reported total.
func readRows(ctx context.Context, sess BrowseSession, ref, table string) (*browse.RowPage, int, error) {
	page, err := sess.Rows(ctx, ref, table, 0, Max)
	if err != nil {
		return nil, 0, err
	}
	return page, page.Total, nil
}

// readRowsOptional is readRows for a table that may not exist: ErrTableNotFound
// degrades to (nil, 0, nil) so the caller can treat it as empty.
func readRowsOptional(ctx context.Context, sess BrowseSession, ref, table string) (*browse.RowPage, int, error) {
	page, err := sess.Rows(ctx, ref, table, 0, Max)
	if err != nil {
		if errors.Is(err, browse.ErrTableNotFound) {
			return nil, 0, nil
		}
		return nil, 0, err
	}
	return page, page.Total, nil
}

// indexCols builds a column-name → cell-index map from a RowPage's Columns, so
// cells are addressed by name regardless of the underlying column order.
func indexCols(cols []string) map[string]int {
	m := make(map[string]int, len(cols))
	for i, c := range cols {
		m[c] = i
	}
	return m
}

// cell returns the named column's value for a row, or "" when the column is
// absent, out of range, or the literal browse NULL placeholder.
func cell(cols map[string]int, row []string, name string) string {
	i, ok := cols[name]
	if !ok || i < 0 || i >= len(row) {
		return ""
	}
	v := row[i]
	if v == "NULL" {
		return ""
	}
	return v
}

// indexStatusCategories maps a status name (lowercased) to its category, from
// the optional custom_statuses table. A nil page — the table is absent — yields
// an empty map, and statusCategory then falls back to its name heuristics.
func indexStatusCategories(statuses *browse.RowPage) map[string]string {
	out := map[string]string{}
	if statuses == nil {
		return out
	}
	cols := indexCols(statuses.Columns)
	for _, r := range statuses.Rows {
		if name := cell(cols, r, "name"); name != "" {
			out[strings.ToLower(name)] = strings.ToLower(cell(cols, r, "category"))
		}
	}
	return out
}

// indexLabels maps issue id → its label names, from the optional labels table.
// A nil page yields an empty map.
func indexLabels(labels *browse.RowPage) map[string][]string {
	out := map[string][]string{}
	if labels == nil {
		return out
	}
	cols := indexCols(labels.Columns)
	for _, r := range labels.Rows {
		id := cell(cols, r, "issue_id")
		lb := cell(cols, r, "label")
		if id != "" && lb != "" {
			out[id] = append(out[id], lb)
		}
	}
	return out
}

// indexIssueCategories maps issue id → status category (open / in_progress /
// closed), which is what decides whether a blocking target still blocks.
func indexIssueCategories(issues *browse.RowPage, cols map[string]int, catByStatus map[string]string) map[string]string {
	out := make(map[string]string, len(issues.Rows))
	for _, r := range issues.Rows {
		out[cell(cols, r, "id")] = statusCategory(cell(cols, r, "status"), catByStatus)
	}
	return out
}

// depIndex is the dependency edge set aggregated per issue: how many things an
// issue waits on, how many wait on it, and whether any of the things it waits on
// is still open (which is what "blocked" means).
type depIndex struct {
	blockedByCount map[string]int  // issue_id → #deps it has
	blocksCount    map[string]int  // depends_on_issue_id → #deps aimed at it
	blockedOpen    map[string]bool // issue_id → has an open blocking dep
}

// indexDeps aggregates the dependencies table. A "blocks" edge to a still-open
// target blocks its source; parent-child is hierarchy, not a blocker — a subtask
// is not blocked by its (open) epic, matching bd's own is_blocked/ready
// accounting. A nil page (the table is absent) yields empty maps.
func indexDeps(deps *browse.RowPage, catByIssue map[string]string) depIndex {
	idx := depIndex{
		blockedByCount: map[string]int{},
		blocksCount:    map[string]int{},
		blockedOpen:    map[string]bool{},
	}
	if deps == nil {
		return idx
	}
	cols := indexCols(deps.Columns)
	for _, r := range deps.Rows {
		from := cell(cols, r, "issue_id")
		to := cell(cols, r, "depends_on_issue_id")
		typ := strings.ToLower(cell(cols, r, "type"))
		if from != "" {
			idx.blockedByCount[from]++
		}
		if to != "" {
			idx.blocksCount[to]++
		}
		if from != "" && typ == "blocks" && catByIssue[to] != "closed" {
			idx.blockedOpen[from] = true
		}
	}
	return idx
}

// readyRow is bd's ready rule, and the only copy of it: an issue is ready when
// it is open (not in-progress, not closed), unblocked, and not a
// template/ephemeral scaffold. Derived in-process from the issues rows already
// loaded rather than by reading the ready_issues table.
//
// Every surface that says "ready" — the board's ⚡ marker and its ?ready=1
// filter, the cross-database /ready page, the MCP ready_work tool — comes
// through here, because two surfaces that each spelled the rule out would
// disagree the first time it changed.
func readyRow(cat string, blocked bool, row []string, cols map[string]int) bool {
	return cat == "open" && !blocked &&
		!truthy(cell(cols, row, "is_template")) && !truthy(cell(cols, row, "ephemeral"))
}

// truthy reports whether a cell reads as a set boolean/flag.
func truthy(s string) bool {
	switch strings.ToLower(strings.TrimSpace(s)) {
	case "1", "true", "yes", "t", "y":
		return true
	}
	return false
}

// sortedKeys returns a set's keys in ascending order.
func sortedKeys(set map[string]bool) []string {
	out := make([]string, 0, len(set))
	for k := range set {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

// containsString reports whether s is in xs.
func containsString(xs []string, s string) bool {
	for _, x := range xs {
		if x == s {
			return true
		}
	}
	return false
}