~bigbes/sr-ht-dolt

ref: c82f106f9e36a8ce14539fdc64d0083b98a90773 sr-ht-dolt/beads/beads.go -rw-r--r-- 4.4 KiB
c82f106f — Eugene Blikh beads: stop flattening a stored NULL string 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
// Package beads is the one reading of the beads (bd) issue schema a hosted Dolt
// database may carry: the table fingerprint, the lane bucketing, the ready rule,
// the status categories, the transitive dependency walk, the event humanizer and
// the milestone rollup.
//
// # Why it is its own package
//
// This reading grew inside web/beads.go, serving one HTML board. It has more
// than one consumer now — the web views and the read-only MCP surface — and a
// consumer that cannot reach it grows its own copy, which is how two surfaces
// start disagreeing quietly about what "ready" or "closed" means. There is one
// fingerprint, one ready rule and one row cap on this instance, and they live
// here.
//
// # What it is allowed to know
//
// Rows in, view model out. This package depends on browse (the row reader) and
// the standard library, and on nothing else in the module: no net/http, no
// html/template, no core. It renders nothing and it authorizes nothing — by the
// time a caller gets here it has already decided that this caller may read this
// database. The structs are plain data carrying a few display-derived methods
// (PriorityLabel, Pct, SubtaskPct) that the HTML templates call on the dot; no
// HTML is built here.
//
// There is no SQL engine behind any of this: a bare NBS store has no working
// set, so every table is read whole (up to Max rows) through the BrowseSession
// seam and projected in process.
package beads

import (
	"context"
	"strings"

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

// BrowseSession is the read-only row surface this package reads a database
// through. It is declared here, consumer-side (the house style web/deps.go
// sets), and names exactly the one method the projections call: everything
// below reads whole tables and projects them, so nothing here needs branches,
// commits or a table listing. web's larger BrowseSession — and *browse.DB
// itself — satisfy it structurally, so a caller hands its own session straight
// through.
type BrowseSession interface {
	// Rows reads a page of a table's rows at refStr. A table that does not exist
	// must report browse.ErrTableNotFound, which readRowsOptional degrades to an
	// empty page for the tables beads treats as optional.
	Rows(ctx context.Context, refStr, table string, offset, limit int) (*browse.RowPage, error)
}

// Max caps how many rows of any single table a projection reads. Beads DBs are
// modest (hundreds–low thousands of issues); if a table exceeds this the board
// notes it is truncated rather than trying to page.
const Max = 2000

// Applies fingerprints a beads DB: both an "issues" and a "dependencies" table
// present, and "issues" carrying at least id + status columns (a cheap guard
// against an unrelated schema that happens to reuse those two table names).
func Applies(tables []browse.TableInfo) bool {
	var haveIssues, haveDeps, haveID, haveStatus bool
	for _, t := range tables {
		switch t.Name {
		case "issues":
			haveIssues = true
			for _, c := range t.Columns {
				switch c.Name {
				case "id":
					haveID = true
				case "status":
					haveStatus = true
				}
			}
		case "dependencies":
			haveDeps = true
		}
	}
	return haveIssues && haveDeps && haveID && haveStatus
}

// statusCategory maps a status name to one of open / in_progress / closed. It
// prefers the custom_statuses lookup and falls back to name heuristics when the
// status is unknown there (or the table was empty).
func statusCategory(status string, catByStatus map[string]string) string {
	s := strings.ToLower(strings.TrimSpace(status))
	if s == "" {
		return "open"
	}
	if cat, ok := catByStatus[s]; ok && cat != "" {
		switch cat {
		case "in_progress", "closed", "open":
			return cat
		}
	}
	switch {
	case strings.Contains(s, "progress"), strings.Contains(s, "doing"), strings.Contains(s, "active"), s == "wip":
		return "in_progress"
	case strings.Contains(s, "close"), strings.Contains(s, "done"), strings.Contains(s, "resolved"), strings.Contains(s, "complete"):
		return "closed"
	default:
		return "open"
	}
}

// laneForCategory returns the lane display name and accent for a status
// category (used by the detail pane; the board buckets inline because it also
// needs the blocked signal).
func laneForCategory(cat string) (name, accent string) {
	switch cat {
	case "closed":
		return "Past Stand", "#868e96"
	case "in_progress":
		return "Rolling", "#c9930a"
	default:
		return "Lined Up", "#2f9e44"
	}
}