~bigbes/sr-ht-dolt

ref: 630735084650ff50de6b40673206da4f2c338198 sr-ht-dolt/beads/model.go -rw-r--r-- 10.9 KiB
63073508 — Eugene Blikh mcpsrv: answer a real NULL as null 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
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package beads

import (
	"net/url"
	"strconv"
	"strings"
)

// Layout names the shape the board mode is rendered in, from ?layout=. It is a
// layout of one view and not a second view: the same filtered set, the same
// buckets, the same cards, either four lanes side by side or one column.
const (
	LayoutBoard  = "board"  // four lanes side by side; the default
	LayoutStream = "stream" // one column, sections stacked in parade order
)

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

// 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 Data struct {
	Mode string // "board" | "detail" | "epic"

	// board mode
	Layout     string // LayoutBoard | LayoutStream; "" in the detail modes
	Lanes      []Lane
	Sections   []Section // LayoutStream only: the same buckets, read top to bottom
	Counts     Counts
	Total      int           // issues placed on the board (after filtering)
	Filter     Filter        // active board filters (sticky form state)
	FilterOpts FilterOptions // distinct values for the filter dropdowns

	// Truncated and ShownOf are set in every mode, board and detail alike: a
	// projection that read only part of a table has to say so wherever it is
	// rendered, or a partial answer reads as a complete one.
	//
	// Truncated says some table this projection read exceeded Max and came back
	// clipped. Which tables that covers is the mode's own set — the board buckets
	// from issues and dependencies; the detail pane also draws on labels,
	// custom_statuses, comments and events.
	Truncated bool
	// ShownOf is the issues table's reported total, clipped or not — what exists,
	// against the rows actually read. IssuesClipped is the comparison callers
	// usually want.
	ShownOf int

	// Query is the request's query as parsed, carried so the layout toggle can
	// rebuild this exact URL with one key replaced (web's withQuery). The view
	// envelope does not carry the query, and rebuilding it from Filter would
	// silently drop everything this projection does not model — ?ref= among
	// them. Set in board mode only.
	Query url.Values

	// detail / epic modes
	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   []TreeNode
	DependentTree []TreeNode

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

// IssuesClipped reports that the issues table itself exceeded Max, so this
// projection saw only its first Max rows. Truncated is the wider fact (any
// input table was clipped); this is the one that decides whether the issue set
// in hand is the whole tracker.
func (d *Data) IssuesClipped() bool { return d.ShownOf > Max }

// Missing reports that a detail build did not find the requested issue: the
// pane has no Issue to render. It says nothing about why — MissingBeyondCap
// does.
func (d *Data) Missing() bool { return d.Mode == "detail" && d.Issue == nil }

// MissingBeyondCap separates the two ways an issue can be missing. False with
// Missing set means the read was complete and there is no such issue. True
// means the issues table was clipped at Max and the id was not among the rows
// read — it may sit in the tail this projection never saw, and a surface that
// answers "no such issue" here is stating something it does not know.
func (d *Data) MissingBeyondCap() bool { return d.Missing() && d.IssuesClipped() }

// 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 Filter struct {
	Query    string // substring match over id + title (case-insensitive)
	Type     string // exact issue_type
	Priority string // exact priority ("0".."3")
	Assignee string // exact assignee
	Label    string // issue must carry this label
	Ready    bool   // only actionable-now issues (bd's `ready` set)
}

// Active reports whether any filter is set (drives the "Clear" link and the
// empty-board wording).
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 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
	}
	if f.Priority != "" && cell(cols, row, "priority") != f.Priority {
		return false
	}
	if f.Assignee != "" && cell(cols, row, "assignee") != f.Assignee {
		return false
	}
	if f.Label != "" && !containsString(labels, f.Label) {
		return false
	}
	if f.Query != "" {
		hay := strings.ToLower(id + " " + cell(cols, row, "title"))
		if !strings.Contains(hay, strings.ToLower(f.Query)) {
			return false
		}
	}
	return true
}

// 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 FilterOptions struct {
	Types      []string
	Priorities []string // "0".."3"
	Assignees  []string
	Labels     []string
}

// 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 []Card
}

// Section is one section of the stream layout: a lane, plus the two things a
// section header in a single column needs that a lane header does not — whether
// it opens collapsed, and a one-line hint at the order its issues are in (the
// stream sorts each section differently, so the order is worth stating).
type Section struct {
	Lane             // Name, Slug, Accent, Issues — the same bucketing as the board
	Collapsed bool   // rendered inside <details> with no open attribute
	Note      string // "" or a one-line hint, e.g. "closed, newest first"
}

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

// Card is one issue as it appears on the board.
type Card struct {
	ID        string
	Title     string
	Type      string
	Priority  string // as stored ("0".."3", ""); PriorityLabel derives the pill
	Assignee  string
	Labels    []string
	BlockedBy int    // # of deps this issue has (things it waits on)
	Blocks    int    // # of deps pointing at this issue (things waiting on it)
	Ready     bool   // actionable now: open, unblocked, not deferred/template (bd's `ready` set)
	Category  string // open | in_progress | closed (used by the milestones view)

	// Sort keys for the stream layout, which orders Rolling by when work was
	// picked up and Past Stand by when it finished. Neither is rendered on a
	// card — the timestamps are on the detail pane, and a card already carries
	// as much metadata as a glance holds. Stored as read ("YYYY-MM-DD HH:MM:SS"),
	// so a lexical compare is a time compare; "" means unset and sorts last.
	StartedAt string
	ClosedAt  string
}

// 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 Card) PriorityLabel() string {
	if c.Priority == "" {
		return ""
	}
	if _, err := strconv.Atoi(c.Priority); err != nil {
		return ""
	}
	return "P" + c.Priority
}

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

// 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 TreeNode struct {
	ID     string
	Title  string
	Type   string
	Status string
	Closed bool
	Depth  int
}

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

// 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 Activity struct {
	Kind      string // "comment" | "event"
	Event     string // events only: the event_type (created/status_changed/updated/closed/…)
	Actor     string
	Summary   string
	Text      string
	CreatedAt string
}

// 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 Subtask struct {
	ID       string
	Title    string
	Status   string
	Category string
	Priority string
	Assignee string
	Blocked  bool
}

// PriorityLabel renders a subtask's numeric priority as a P-pill ("P0".."P3"),
// or "" when unset/unparseable.
func (s Subtask) PriorityLabel() string {
	if s.Priority == "" {
		return ""
	}
	if _, err := strconv.Atoi(s.Priority); err != nil {
		return ""
	}
	return "P" + s.Priority
}

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

// 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 Issue struct {
	ID                 string
	Title              string
	Status             string
	Lane               string
	Accent             string
	Priority           string
	IssueType          string
	Assignee           string
	CreatedBy          string
	Owner              string
	EstimatedMinutes   string
	ExternalRef        string
	SpecID             string
	Description        string
	Design             string
	AcceptanceCriteria string
	Notes              string
	CreatedAt          string
	StartedAt          string
	UpdatedAt          string
	ClosedAt           string
	CloseReason        string
	Labels             []string
}