// 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" } }