~bigbes/sr-ht-dolt

ref: 377c616a8a3b385f18a8d6509c7a6be3a31af2bd sr-ht-dolt/beads/rows.go -rw-r--r-- 8.7 KiB
377c616a — Eugene Blikh web: render memory bodies as markdown, and resolve their references 3 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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
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
}

// rowCells is one row of a page together with the NULL mask browse returned
// beside it: the rendered strings, and the answer to "does this cell hold a
// value at all?" that the strings cannot carry.
//
// It is what every projection here iterates and what cell reads, so a row and
// its mask travel together and cannot be paired up wrongly at a call site.
type rowCells struct {
	values []string

	// nulls is browse's mask for this row, or nil for a page that carried none —
	// which is a hand-built page, since browse fills one for every page it
	// returns. See cell for what an absent mask means.
	nulls []bool
}

// rowsOf pairs each row of a page with its own mask. A nil page — an optional
// table that is absent — has no rows, which is what the callers of
// readRowsOptional already treat it as.
//
// A row the mask does not cover gets a nil one rather than an all-false one: not
// knowing whether a cell holds a value is a different answer from knowing that
// it does, and cell reads the two differently.
func rowsOf(page *browse.RowPage) []rowCells {
	if page == nil {
		return nil
	}
	out := make([]rowCells, 0, len(page.Rows))
	for i, r := range page.Rows {
		var mask []bool
		if i < len(page.Nulls) {
			mask = page.Nulls[i]
		}
		out = append(out, rowCells{values: r, nulls: mask})
	}
	return out
}

// cell returns the named column's value for a row, or "" when the column is
// absent, out of range, or holds no value at all.
//
// A cell that holds no value reads as "": every projection renders a missing
// timestamp, assignee or close reason as nothing, and that must not change. What
// the mask changes is the other reading of the same string — browse renders a
// real NULL as the text "NULL", so a row that *stores* those four characters
// rendered identically and was flattened to "" too, which turned a stored title
// into an empty one. The mask beside the row answers which of the two it is, so
// it decides here rather than the string.
//
// A cell no mask covers is read the way this package read every cell before the
// mask existed: "NULL" is absent. That is not a guess dressed up as an answer —
// it is the older reading, kept for the only pages that lack a mask, which are
// the ones built by hand rather than read from a store. browse fills a mask
// parallel to the rows for every page it returns, so no read of a database
// arrives here without one.
func cell(cols map[string]int, row rowCells, name string) string {
	i, ok := cols[name]
	if !ok || i < 0 || i >= len(row.values) {
		return ""
	}
	if row.isNull(i) {
		return ""
	}
	return row.values[i]
}

// isNull reports whether the i-th cell of this row holds no value.
//
// It is the one place in this package that decides, so cell — which flattens an
// absent cell to "" — and the raw section — which must not — can never come to
// different answers about the same cell. The reading is the one cell documents:
// the mask decides when the row has one, and a row with none falls back to the
// pre-mask reading, where the text "NULL" is how absence arrived.
//
// A cell past the end of the row is not a value, so it reads as absent.
func (r rowCells) isNull(i int) bool {
	if i < 0 || i >= len(r.values) {
		return true
	}
	if i < len(r.nulls) {
		return r.nulls[i]
	}
	return r.values[i] == "NULL"
}

// 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 rowsOf(statuses) {
		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 rowsOf(labels) {
		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 rowsOf(issues) {
		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 rowsOf(deps) {
		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 rowCells, 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
}