~bigbes/sr-ht-dolt

7d799ed0ff8809cdad362a092d4437bd72d58a82 — Eugene Blikh 5 days ago f9c82b0
beads: drop the Bead/Beads prefix from the moved types
M beads/beads_test.go => beads/beads_test.go +4 -4
@@ 120,7 120,7 @@ func beadsFixture() *fakeSession {
}

// laneBySlug finds a lane in a built board by its slug.
func laneBySlug(d *BeadsData, slug string) *BeadsLane {
func laneBySlug(d *Data, slug string) *Lane {
	for i := range d.Lanes {
		if d.Lanes[i].Slug == slug {
			return &d.Lanes[i]


@@ 130,7 130,7 @@ func laneBySlug(d *BeadsData, slug string) *BeadsLane {
}

// cardIDs lists the ids of a lane's cards.
func cardIDs(l *BeadsLane) []string {
func cardIDs(l *Lane) []string {
	if l == nil {
		return nil
	}


@@ 177,7 177,7 @@ func TestBeadsBuildBoardLanes(t *testing.T) {
		assert.Equal(t, want, cardIDs(laneBySlug(d, slug)), "lane %s", slug)
	}

	assert.Equal(t, BeadsCounts{Rolling: 1, LinedUp: 1, Stalled: 1, PastStand: 1, Total: 4}, d.Counts)
	assert.Equal(t, Counts{Rolling: 1, LinedUp: 1, Stalled: 1, PastStand: 1, Total: 4}, d.Counts)
	assert.Equal(t, 4, d.Total)
}



@@ 199,7 199,7 @@ func TestBeadsBuildBoardCounts(t *testing.T) {
}

// boardIDs returns every card id on the board, across all lanes.
func boardIDs(d *BeadsData) []string {
func boardIDs(d *Data) []string {
	var out []string
	for i := range d.Lanes {
		out = append(out, cardIDs(&d.Lanes[i])...)

M beads/build.go => beads/build.go +22 -22
@@ 14,7 14,7 @@ import (

// Build reads the issue graph and produces either the board or, when ?issue=
// names an issue, that issue's detail pane.
func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values) (*BeadsData, error) {
func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values) (*Data, error) {
	issues, issuesTotal, err := readRows(ctx, sess, ref, "issues")
	if err != nil {
		return nil, err


@@ 98,7 98,7 @@ func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values

	// Board mode: parse the sticky filters and collect dropdown options from the
	// full issue set (options stay stable as filters narrow the board).
	filter := BeadsFilter{
	filter := Filter{
		Query:    strings.TrimSpace(query.Get("q")),
		Type:     query.Get("type"),
		Priority: query.Get("priority"),


@@ 109,7 109,7 @@ func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values
	opts := collectFilterOptions(issues, issueCols, labelsByIssue)

	// Bucket every matching issue into exactly one lane.
	var rolling, linedUp, stalled, pastStand []BeadCard
	var rolling, linedUp, stalled, pastStand []Card
	for _, r := range issues.Rows {
		id := cell(issueCols, r, "id")
		if !filter.matches(id, r, issueCols, labelsByIssue[id]) {


@@ 125,7 125,7 @@ func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values
		if filter.Ready && !ready {
			continue
		}
		card := BeadCard{
		card := Card{
			ID:        id,
			Title:     cell(issueCols, r, "title"),
			Type:      cell(issueCols, r, "issue_type"),


@@ 150,13 150,13 @@ func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values
	}

	created := issueCreatedAt(issues, issueCols)
	for _, lane := range [][]BeadCard{rolling, linedUp, stalled, pastStand} {
	for _, lane := range [][]Card{rolling, linedUp, stalled, pastStand} {
		sortCards(lane, created)
	}

	data := &BeadsData{
	data := &Data{
		Mode: "board",
		Lanes: []BeadsLane{
		Lanes: []Lane{
			// Accents are muted Mardi Gras hues (gold / green / violet / gray)
			// chosen to read on both the light and dark SourceHut themes. They
			// are applied by the template as thin accents (card border, lane


@@ 166,7 166,7 @@ func Build(ctx context.Context, sess BrowseSession, ref string, query url.Values
			{Name: "Stalled", Slug: "stalled", Accent: "#9c36b5", Issues: stalled},
			{Name: "Past Stand", Slug: "past-stand", Accent: "#868e96", Issues: pastStand},
		},
		Counts: BeadsCounts{
		Counts: Counts{
			Rolling:   len(rolling),
			LinedUp:   len(linedUp),
			Stalled:   len(stalled),


@@ 193,7 193,7 @@ func buildDetail(
	deps *browse.RowPage, depCols map[string]int,
	labelsByIssue map[string][]string,
	catByStatus, catByIssue map[string]string,
) *BeadsData {
) *Data {
	// id → (title, status, whole row) for edge labels and the subtask rollup.
	titleByIssue := map[string]string{}
	statusByIssue := map[string]string{}


@@ 209,7 209,7 @@ func buildDetail(
		}
	}

	data := &BeadsData{Mode: "detail"}
	data := &Data{Mode: "detail"}
	if row == nil {
		// Unknown id: a detail pane with a nil Issue; the template shows a
		// "not found" note and a link back to the board.


@@ 224,7 224,7 @@ func buildDetail(

	status := cell(issueCols, row, "status")
	name, accent := laneForCategory(statusCategory(status, catByStatus))
	data.Issue = &BeadIssue{
	data.Issue = &Issue{
		ID:                 want,
		Title:              cell(issueCols, row, "title"),
		Status:             status,


@@ 250,9 250,9 @@ func buildDetail(
		Labels:             labelsByIssue[want],
	}

	edge := func(id, typ string) BeadEdge {
	edge := func(id, typ string) Edge {
		st := statusByIssue[id]
		return BeadEdge{
		return Edge{
			IssueID: id,
			Title:   titleByIssue[id],
			Type:    typ,


@@ 274,7 274,7 @@ func buildDetail(
			if data.Mode == "epic" && strings.EqualFold(typ, "parent-child") {
				cr := rowByID[from]
				cat := catByIssue[from]
				st := BeadSubtask{
				st := Subtask{
					ID:       from,
					Title:    titleByIssue[from],
					Status:   statusByIssue[from],


@@ 333,8 333,8 @@ func buildDetail(
			author := cell(ccols, r, "author")
			text := cell(ccols, r, "text")
			at := cell(ccols, r, "created_at")
			data.Comments = append(data.Comments, BeadComment{Author: author, Text: text, CreatedAt: at})
			data.History = append(data.History, BeadActivity{
			data.Comments = append(data.Comments, Comment{Author: author, Text: text, CreatedAt: at})
			data.History = append(data.History, Activity{
				Kind:      "comment",
				Actor:     author,
				Summary:   "commented",


@@ 355,7 355,7 @@ func buildDetail(
			et := cell(ecols, r, "event_type")
			summary, text := humanizeEvent(et,
				cell(ecols, r, "old_value"), cell(ecols, r, "new_value"), cell(ecols, r, "comment"))
			data.History = append(data.History, BeadActivity{
			data.History = append(data.History, Activity{
				Kind:      "event",
				Event:     et,
				Actor:     cell(ecols, r, "actor"),


@@ 372,7 372,7 @@ func buildDetail(

// collectFilterOptions gathers the distinct issue_type / priority / assignee
// values and label names across all issues, sorted, for the filter dropdowns.
func collectFilterOptions(issues *browse.RowPage, cols map[string]int, labelsByIssue map[string][]string) BeadsFilterOptions {
func collectFilterOptions(issues *browse.RowPage, cols map[string]int, labelsByIssue map[string][]string) FilterOptions {
	types, prios, assignees, labels := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
	for _, r := range issues.Rows {
		if t := cell(cols, r, "issue_type"); t != "" {


@@ 390,7 390,7 @@ func collectFilterOptions(issues *browse.RowPage, cols map[string]int, labelsByI
			labels[l] = true
		}
	}
	return BeadsFilterOptions{
	return FilterOptions{
		Types:      sortedKeys(types),
		Priorities: sortedKeys(prios), // single digits sort numerically as strings
		Assignees:  sortedKeys(assignees),


@@ 409,7 409,7 @@ func issueCreatedAt(issues *browse.RowPage, cols map[string]int) map[string]stri

// sortCards orders a lane by priority (0 = highest first), then created_at
// ascending, then id — a stable, deterministic parade order.
func sortCards(cards []BeadCard, created map[string]string) {
func sortCards(cards []Card, created map[string]string) {
	sort.SliceStable(cards, func(i, j int) bool {
		pi, pj := priorityRank(cards[i].Priority), priorityRank(cards[j].Priority)
		if pi != pj {


@@ 439,7 439,7 @@ func priorityRank(p string) int {
// sortSubtasks orders an epic's children open-work-first: unclosed before
// closed, then by priority (0 highest), then id — closed subtasks sink to the
// bottom so the actionable ones lead.
func sortSubtasks(subs []BeadSubtask) {
func sortSubtasks(subs []Subtask) {
	sort.SliceStable(subs, func(i, j int) bool {
		ci, cj := subs[i].Category == "closed", subs[j].Category == "closed"
		if ci != cj {


@@ 456,7 456,7 @@ func sortSubtasks(subs []BeadSubtask) {
// sortActivity orders the merged history oldest-first (chronological). Timestamps
// share the "YYYY-MM-DD HH:MM:SS" shape across events and comments, so a lexical
// compare is a time compare; ties fall back to id-free but stable order.
func sortActivity(acts []BeadActivity) {
func sortActivity(acts []Activity) {
	sort.SliceStable(acts, func(i, j int) bool {
		return acts[i].CreatedAt < acts[j].CreatedAt
	})

M beads/deps.go => beads/deps.go +9 -9
@@ 20,8 20,8 @@ type depLink struct {
// pre-order, flattening the reachable set into indented nodes. Each issue
// appears once (first path wins); depth and node count are bounded so a dense
// or cyclic graph is safe.
func buildDepTree(root string, adj map[string][]depLink, titleOf, statusOf, catByStatus map[string]string) []BeadTreeNode {
	var out []BeadTreeNode
func buildDepTree(root string, adj map[string][]depLink, titleOf, statusOf, catByStatus map[string]string) []TreeNode {
	var out []TreeNode
	visited := map[string]bool{root: true}
	var dfs func(id string, depth int)
	dfs = func(id string, depth int) {


@@ 34,7 34,7 @@ func buildDepTree(root string, adj map[string][]depLink, titleOf, statusOf, catB
			}
			visited[lnk.to] = true
			st := statusOf[lnk.to]
			out = append(out, BeadTreeNode{
			out = append(out, TreeNode{
				ID:     lnk.to,
				Title:  titleOf[lnk.to],
				Type:   lnk.typ,


@@ 51,7 51,7 @@ func buildDepTree(root string, adj map[string][]depLink, titleOf, statusOf, catB

// hasTransitive reports whether a flattened tree reaches past the direct edges
// (any Depth>0 node) — the signal that it adds something the flat list doesn't.
func hasTransitive(nodes []BeadTreeNode) bool {
func hasTransitive(nodes []TreeNode) bool {
	for _, n := range nodes {
		if n.Depth > 0 {
			return true


@@ 66,9 66,9 @@ func hasTransitive(nodes []BeadTreeNode) bool {
// linked under an epic — still appear on the timeline. Returns ok=false when the
// edge does not touch `want` or the row has no timestamp (older schema without
// created_at: skip rather than emit a blank-dated entry).
func depActivity(want, from, to, typ, at, by string) (BeadActivity, bool) {
func depActivity(want, from, to, typ, at, by string) (Activity, bool) {
	if at == "" || (from != want && to != want) {
		return BeadActivity{}, false
		return Activity{}, false
	}
	var summary string
	switch strings.ToLower(strings.TrimSpace(typ)) {


@@ 88,15 88,15 @@ func depActivity(want, from, to, typ, at, by string) (BeadActivity, bool) {
		// Related is symmetric; emit once (from the issue_id side) to avoid a
		// duplicate entry on both endpoints.
		if from != want {
			return BeadActivity{}, false
			return Activity{}, false
		}
		summary = "linked " + to + " (related)"
	default:
		if from == want {
			summary = "added " + typ + " dependency on " + to
		} else {
			return BeadActivity{}, false
			return Activity{}, false
		}
	}
	return BeadActivity{Kind: "dep", Event: "dependency", Actor: by, Summary: summary, CreatedAt: at}, true
	return Activity{Kind: "dep", Event: "dependency", Actor: by, Summary: summary, CreatedAt: at}, true
}

M beads/milestones.go => beads/milestones.go +9 -9
@@ 26,18 26,18 @@ type MilestoneDetail struct {
	Done       int // closed
	InProgress int
	Open       int             // open (or unknown) — the remaining work
	Heads      []BeadCard      // issue_type == "milestone" — the milestone's own issue(s)
	Heads      []Card          // issue_type == "milestone" — the milestone's own issue(s)
	Epics      []MilestoneEpic // epics in the milestone, each with its nested subtasks
	Loose      []BeadCard      // members that are neither heads, epics, nor nested subtasks
	Loose      []Card          // members that are neither heads, epics, nor nested subtasks
}

// MilestoneEpic is an epic inside a milestone together with the milestone
// members nested under it (parent-child edges pointing at the epic).
type MilestoneEpic struct {
	Card     BeadCard
	Card     Card
	Done     int // closed children, for the "d/t" rollup on the epic row
	Total    int
	Children []BeadCard
	Children []Card
}

// Pct is the milestone's completion percentage (0..100) for the progress bar.


@@ 100,12 100,12 @@ func BuildMilestones(ctx context.Context, sess BrowseSession, ref string) (*Mile

	issueCols := indexCols(issues.Columns)
	byLabel := map[string]*MilestoneDetail{}
	cardsByLabel := map[string][]BeadCard{}
	cardsByLabel := map[string][]Card{}
	unlabeled := 0
	for _, r := range issues.Rows {
		id := cell(issueCols, r, "id")
		cat := statusCategory(cell(issueCols, r, "status"), catByStatus)
		card := BeadCard{
		card := Card{
			ID:       id,
			Title:    cell(issueCols, r, "title"),
			Type:     cell(issueCols, r, "issue_type"),


@@ 160,7 160,7 @@ func BuildMilestones(ctx context.Context, sess BrowseSession, ref string) (*Mile
// under them, then the leftovers. Only members of this milestone participate —
// membership stays purely label-based; a child nests only when its epic carries
// the same milestone label.
func (md *MilestoneDetail) arrange(cards []BeadCard, parentsByChild map[string][]string) {
func (md *MilestoneDetail) arrange(cards []Card, parentsByChild map[string][]string) {
	epicByID := map[string]*MilestoneEpic{}
	var epics []*MilestoneEpic
	for _, c := range cards {


@@ 208,13 208,13 @@ func (md *MilestoneDetail) arrange(cards []BeadCard, parentsByChild map[string][

// sortMilestoneCards orders a milestone's issues open-work-first (closed sinks to
// the bottom), then by priority, then id — a stable, deterministic order.
func sortMilestoneCards(cards []BeadCard) {
func sortMilestoneCards(cards []Card) {
	sort.SliceStable(cards, func(i, j int) bool {
		return milestoneCardLess(cards[i], cards[j])
	})
}

func milestoneCardLess(a, b BeadCard) bool {
func milestoneCardLess(a, b Card) bool {
	ca, cb := a.Category == "closed", b.Category == "closed"
	if ca != cb {
		return !ca

M beads/model.go => beads/model.go +45 -45
@@ 7,44 7,44 @@ import (

// --- view model --------------------------------------------------------------

// BeadsData is the opaque .Data value handed to beads.html. Mode discriminates
// Data is the opaque .Data value handed to beads.html. Mode discriminates
// the renderings: "board" (all lanes), "detail" (one issue), or "epic" (a
// detail whose issue is an epic, which also carries its subtask rollup).
type BeadsData struct {
type Data struct {
	Mode string // "board" | "detail" | "epic"

	// board mode
	Lanes      []BeadsLane
	Counts     BeadsCounts
	Total      int                // issues placed on the board (after filtering)
	Truncated  bool               // an input table exceeded Max and was clipped
	ShownOf    int                // when Truncated: the reported table total
	Filter     BeadsFilter        // active board filters (sticky form state)
	FilterOpts BeadsFilterOptions // distinct values for the filter dropdowns
	Lanes      []Lane
	Counts     Counts
	Total      int           // issues placed on the board (after filtering)
	Truncated  bool          // an input table exceeded Max and was clipped
	ShownOf    int           // when Truncated: the reported table total
	Filter     Filter        // active board filters (sticky form state)
	FilterOpts FilterOptions // distinct values for the filter dropdowns

	// detail / epic modes
	Issue        *BeadIssue
	DependsOn    []BeadEdge     // this issue depends on … (outgoing, direct)
	DependedOnBy []BeadEdge     // … is depended on by this issue (incoming, direct)
	Comments     []BeadComment  // the comment thread (Comments tab)
	History      []BeadActivity // comments + audit events, time-sorted (History tab)
	Issue        *Issue
	DependsOn    []Edge     // this issue depends on … (outgoing, direct)
	DependedOnBy []Edge     // … is depended on by this issue (incoming, direct)
	Comments     []Comment  // the comment thread (Comments tab)
	History      []Activity // comments + audit events, time-sorted (History tab)

	// Transitive dependency trees (flattened, pre-order with Depth), shown only
	// when they reach past the direct edges. DependsTree is the full prerequisite
	// chain; DependentTree is everything this issue transitively unblocks.
	DependsTree   []BeadTreeNode
	DependentTree []BeadTreeNode
	DependsTree   []TreeNode
	DependentTree []TreeNode

	// epic mode: the issue's parent-child children and their rollup.
	Subtasks     []BeadSubtask
	Subtasks     []Subtask
	SubtaskDone  int // # of subtasks in the closed category
	SubtaskTotal int // len(Subtasks); the progress denominator
}

// BeadsFilter holds the active board filters, parsed from the query string and
// Filter holds the active board filters, parsed from the query string and
// echoed back into the form so selections stick across submits. Empty fields
// mean "no constraint".
type BeadsFilter struct {
type Filter struct {
	Query    string // substring match over id + title (case-insensitive)
	Type     string // exact issue_type
	Priority string // exact priority ("0".."3")


@@ 55,12 55,12 @@ type BeadsFilter struct {

// Active reports whether any filter is set (drives the "Clear" link and the
// empty-board wording).
func (f BeadsFilter) Active() bool {
func (f Filter) Active() bool {
	return f.Query != "" || f.Type != "" || f.Priority != "" || f.Assignee != "" || f.Label != "" || f.Ready
}

// matches reports whether one issue row passes every set filter.
func (f BeadsFilter) matches(id string, row []string, cols map[string]int, labels []string) bool {
func (f Filter) matches(id string, row []string, cols map[string]int, labels []string) bool {
	if f.Type != "" && cell(cols, row, "issue_type") != f.Type {
		return false
	}


@@ 82,26 82,26 @@ func (f BeadsFilter) matches(id string, row []string, cols map[string]int, label
	return true
}

// BeadsFilterOptions lists the distinct values present across all issues, so the
// FilterOptions lists the distinct values present across all issues, so the
// filter dropdowns offer only real choices. Collected from the unfiltered set so
// the options don't shrink as a filter narrows the board.
type BeadsFilterOptions struct {
type FilterOptions struct {
	Types      []string
	Priorities []string // "0".."3"
	Assignees  []string
	Labels     []string
}

// BeadsLane is one parade lane and the cards in it.
type BeadsLane struct {
// Lane is one parade lane and the cards in it.
type Lane struct {
	Name   string // human label, e.g. "Rolling"
	Slug   string // css-safe identifier, e.g. "rolling"
	Accent string // hex accent color for the lane header/border
	Issues []BeadCard
	Issues []Card
}

// BeadsCounts is the marquee: per-lane totals plus the grand total.
type BeadsCounts struct {
// Counts is the marquee: per-lane totals plus the grand total.
type Counts struct {
	Rolling   int
	LinedUp   int
	Stalled   int


@@ 109,8 109,8 @@ type BeadsCounts struct {
	Total     int
}

// BeadCard is one issue as it appears on the board.
type BeadCard struct {
// Card is one issue as it appears on the board.
type Card struct {
	ID        string
	Title     string
	Type      string


@@ 125,7 125,7 @@ type BeadCard struct {

// PriorityLabel renders the numeric priority as a P-pill label ("P0".."P3"),
// or "" when unset/unparseable so the template can omit the marker.
func (c BeadCard) PriorityLabel() string {
func (c Card) PriorityLabel() string {
	if c.Priority == "" {
		return ""
	}


@@ 135,8 135,8 @@ func (c BeadCard) PriorityLabel() string {
	return "P" + c.Priority
}

// BeadEdge is one dependency edge to another issue, linked in the detail pane.
type BeadEdge struct {
// Edge is one dependency edge to another issue, linked in the detail pane.
type Edge struct {
	IssueID string
	Title   string
	Type    string


@@ 144,10 144,10 @@ type BeadEdge struct {
	Closed  bool
}

// BeadTreeNode is one node in a flattened transitive dependency tree. Depth is
// TreeNode is one node in a flattened transitive dependency tree. Depth is
// the indentation level (0 = a direct edge of the root issue); Type is the
// dependency type of the edge that reached this node.
type BeadTreeNode struct {
type TreeNode struct {
	ID     string
	Title  string
	Type   string


@@ 156,18 156,18 @@ type BeadTreeNode struct {
	Depth  int
}

// BeadComment is one row of the comments thread.
type BeadComment struct {
// Comment is one row of the comments thread.
type Comment struct {
	Author    string
	Text      string
	CreatedAt string
}

// BeadActivity is one entry in the merged history timeline: either a comment or
// Activity is one entry in the merged history timeline: either a comment or
// an audit event from the events table. Summary is a human-readable one-liner
// ("changed status to in_progress"); Text carries the comment body or an event's
// free-text note. Kind drives the icon/label in the template.
type BeadActivity struct {
type Activity struct {
	Kind      string // "comment" | "event"
	Event     string // events only: the event_type (created/status_changed/updated/closed/…)
	Actor     string


@@ 176,10 176,10 @@ type BeadActivity struct {
	CreatedAt string
}

// BeadSubtask is one child of an epic — the "from" side of a parent-child
// Subtask is one child of an epic — the "from" side of a parent-child
// dependency that points at the epic. Category (open/in_progress/closed) drives
// the status accent and feeds the epic's progress rollup.
type BeadSubtask struct {
type Subtask struct {
	ID       string
	Title    string
	Status   string


@@ 191,7 191,7 @@ type BeadSubtask struct {

// PriorityLabel renders a subtask's numeric priority as a P-pill ("P0".."P3"),
// or "" when unset/unparseable.
func (s BeadSubtask) PriorityLabel() string {
func (s Subtask) PriorityLabel() string {
	if s.Priority == "" {
		return ""
	}


@@ 203,18 203,18 @@ func (s BeadSubtask) PriorityLabel() string {

// SubtaskPct is the epic's completion percentage (0..100), for the progress bar
// width. Zero subtasks reads as 0%.
func (d *BeadsData) SubtaskPct() int {
func (d *Data) SubtaskPct() int {
	if d.SubtaskTotal == 0 {
		return 0
	}
	return d.SubtaskDone * 100 / d.SubtaskTotal
}

// BeadIssue is the full issue shown in the detail pane. The field set mirrors
// Issue is the full issue shown in the detail pane. The field set mirrors
// the user-facing columns bd surfaces for an issue (see `bd show`): identity and
// status, the four long-text bodies, effort/reference metadata, the full
// timestamp trail, and the close reason recorded when an issue is resolved.
type BeadIssue struct {
type Issue struct {
	ID                 string
	Title              string
	Status             string

M web/beads.go => web/beads.go +1 -1
@@ 30,7 30,7 @@ func (*beadsView) Template() string { return "beads.html" }
func (*beadsView) Applies(tables []browse.TableInfo) bool { return beads.Applies(tables) }

// Build reads the issue graph and produces either the board or, when ?issue=
// names an issue, that issue's detail pane. The result is a *beads.BeadsData,
// names an issue, that issue's detail pane. The result is a *beads.Data,
// handed to beads.html as its .Data.
func (*beadsView) Build(ctx context.Context, sess BrowseSession, _ *core.Repo, ref string, query url.Values) (any, error) {
	data, err := beads.Build(ctx, sess, ref, query)

M web/realdata_test.go => web/realdata_test.go +3 -3
@@ 63,13 63,13 @@ func TestRealBeadsStore(t *testing.T) {
	if err != nil {
		t.Fatalf("board build: %v", err)
	}
	bd := board.(*beads.BeadsData)
	bd := board.(*beads.Data)
	t.Logf("board total=%d types=%v priorities=%v labels=%v",
		bd.Total, bd.FilterOpts.Types, bd.FilterOpts.Priorities, bd.FilterOpts.Labels)
	if len(bd.FilterOpts.Types) > 0 {
		ft := bd.FilterOpts.Types[0]
		filtered, _ := v.Build(ctx, dbh, nil, ref, url.Values{"type": {ft}})
		t.Logf("filter type=%q → %d issues (of %d)", ft, filtered.(*beads.BeadsData).Total, bd.Total)
		t.Logf("filter type=%q → %d issues (of %d)", ft, filtered.(*beads.Data).Total, bd.Total)
	}
	readyN := 0
	for i := range bd.Lanes {


@@ 106,7 106,7 @@ func TestRealBeadsStore(t *testing.T) {
	if err != nil {
		t.Fatalf("build: %v", err)
	}
	data := raw.(*beads.BeadsData)
	data := raw.(*beads.Data)
	if data.Issue == nil {
		t.Fatalf("issue %q not found", want)
	}