| 1 |
|
// Package beads is the one reading of the beads (bd) issue schema a hosted Dolt |
| 2 |
|
// database may carry: the table fingerprint, the lane bucketing, the ready rule, |
| 3 |
|
// the status categories, the transitive dependency walk, the event humanizer and |
| 4 |
|
// the milestone rollup. |
| 5 |
|
// |
| 6 |
|
// # Why it is its own package |
| 7 |
|
// |
| 8 |
|
// This reading grew inside web/beads.go, serving one HTML board. It has more |
| 9 |
|
// than one consumer now — the web views and the read-only MCP surface — and a |
| 10 |
|
// consumer that cannot reach it grows its own copy, which is how two surfaces |
| 11 |
|
// start disagreeing quietly about what "ready" or "closed" means. There is one |
| 12 |
|
// fingerprint, one ready rule and one row cap on this instance, and they live |
| 13 |
|
// here. |
| 14 |
|
// |
| 15 |
|
// # What it is allowed to know |
| 16 |
|
// |
| 17 |
|
// Rows in, view model out. This package depends on browse (the row reader) and |
| 18 |
|
// the standard library, and on nothing else in the module: no net/http, no |
| 19 |
|
// html/template, no core. It renders nothing and it authorizes nothing — by the |
| 20 |
|
// time a caller gets here it has already decided that this caller may read this |
| 21 |
|
// database. The structs are plain data carrying a few display-derived methods |
| 22 |
|
// (PriorityLabel, Pct, SubtaskPct) that the HTML templates call on the dot; no |
| 23 |
|
// HTML is built here. |
| 24 |
|
// |
| 25 |
|
// There is no SQL engine behind any of this: a bare NBS store has no working |
| 26 |
|
// set, so every table is read whole (up to Max rows) through the BrowseSession |
| 27 |
|
// seam and projected in process. |
| 28 |
|
package beads |
| 29 |
|
|
| 30 |
|
import ( |
| 31 |
|
"context" |
| 32 |
|
"strings" |
| 33 |
|
|
| 34 |
|
"sourcecraft.dev/bigbes/sr-ht-dolt/browse" |
| 35 |
|
) |
| 36 |
|
|
| 37 |
|
// BrowseSession is the read-only row surface this package reads a database |
| 38 |
|
// through. It is declared here, consumer-side (the house style web/deps.go |
| 39 |
|
// sets), and names exactly the one method the projections call: everything |
| 40 |
|
// below reads whole tables and projects them, so nothing here needs branches, |
| 41 |
|
// commits or a table listing. web's larger BrowseSession — and *browse.DB |
| 42 |
|
// itself — satisfy it structurally, so a caller hands its own session straight |
| 43 |
|
// through. |
| 44 |
|
type BrowseSession interface { |
| 45 |
|
// Rows reads a page of a table's rows at refStr. A table that does not exist |
| 46 |
|
// must report browse.ErrTableNotFound, which readRowsOptional degrades to an |
| 47 |
|
// empty page for the tables beads treats as optional. |
| 48 |
|
Rows(ctx context.Context, refStr, table string, offset, limit int) (*browse.RowPage, error) |
| 49 |
|
} |
| 50 |
|
|
| 51 |
|
// Max caps how many rows of any single table a projection reads. Beads DBs are |
| 52 |
|
// modest (hundreds–low thousands of issues); if a table exceeds this the board |
| 53 |
|
// notes it is truncated rather than trying to page. |
| 54 |
|
const Max = 2000 |
| 55 |
|
|
| 56 |
|
// Applies fingerprints a beads DB: both an "issues" and a "dependencies" table |
| 57 |
|
// present, and "issues" carrying at least id + status columns (a cheap guard |
| 58 |
|
// against an unrelated schema that happens to reuse those two table names). |
| 59 |
108 |
func Applies(tables []browse.TableInfo) bool { |
| 60 |
108 |
var haveIssues, haveDeps, haveID, haveStatus bool |
| 61 |
317 |
for _, t := range tables { |
| 62 |
317 |
switch t.Name { |
| 63 |
105 |
case "issues": |
| 64 |
105 |
haveIssues = true |
| 65 |
209 |
for _, c := range t.Columns { |
| 66 |
209 |
switch c.Name { |
| 67 |
105 |
case "id": |
| 68 |
105 |
haveID = true |
| 69 |
104 |
case "status": |
| 70 |
104 |
haveStatus = true |
| 71 |
|
} |
| 72 |
|
} |
| 73 |
104 |
case "dependencies": |
| 74 |
104 |
haveDeps = true |
| 75 |
|
} |
| 76 |
|
} |
| 77 |
108 |
return haveIssues && haveDeps && haveID && haveStatus |
| 78 |
|
} |
| 79 |
|
|
| 80 |
|
// statusCategory maps a status name to one of open / in_progress / closed. It |
| 81 |
|
// prefers the custom_statuses lookup and falls back to name heuristics when the |
| 82 |
|
// status is unknown there (or the table was empty). |
| 83 |
22852 |
func statusCategory(status string, catByStatus map[string]string) string { |
| 84 |
22852 |
s := strings.ToLower(strings.TrimSpace(status)) |
| 85 |
22852 |
if s == "" { |
| 86 |
0 |
return "open" |
| 87 |
0 |
} |
| 88 |
22852 |
if cat, ok := catByStatus[s]; ok && cat != "" { |
| 89 |
14615 |
switch cat { |
| 90 |
14615 |
case "in_progress", "closed", "open": |
| 91 |
14615 |
return cat |
| 92 |
|
} |
| 93 |
|
} |
| 94 |
8237 |
switch { |
| 95 |
2021 |
case strings.Contains(s, "progress"), strings.Contains(s, "doing"), strings.Contains(s, "active"), s == "wip": |
| 96 |
2021 |
return "in_progress" |
| 97 |
4024 |
case strings.Contains(s, "close"), strings.Contains(s, "done"), strings.Contains(s, "resolved"), strings.Contains(s, "complete"): |
| 98 |
4024 |
return "closed" |
| 99 |
2192 |
default: |
| 100 |
2192 |
return "open" |
| 101 |
|
} |
| 102 |
|
} |
| 103 |
|
|
| 104 |
|
// laneForCategory returns the lane display name and accent for a status |
| 105 |
|
// category (used by the detail pane; the board buckets inline because it also |
| 106 |
|
// needs the blocked signal). |
| 107 |
20 |
func laneForCategory(cat string) (name, accent string) { |
| 108 |
20 |
switch cat { |
| 109 |
2 |
case "closed": |
| 110 |
2 |
return "Past Stand", "#868e96" |
| 111 |
2 |
case "in_progress": |
| 112 |
2 |
return "Rolling", "#c9930a" |
| 113 |
16 |
default: |
| 114 |
16 |
return "Lined Up", "#2f9e44" |
| 115 |
|
} |
| 116 |
|
} |