@omercnet/paseo-beads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ import type { BeadSummary } from "../shared/beads";
2
+
3
+ export const BEAD_LANES = ["ready", "in_progress", "blocked", "other"] as const;
4
+
5
+ export type BeadLane = (typeof BEAD_LANES)[number];
6
+
7
+ export const BEAD_LANE_TITLES: Record<BeadLane, string> = {
8
+ ready: "Ready",
9
+ in_progress: "In progress",
10
+ blocked: "Blocked",
11
+ other: "Other",
12
+ };
13
+
14
+ export const BEAD_LANE_METADATA: readonly { id: BeadLane; title: string }[] = BEAD_LANES.map(
15
+ (id) => ({ id, title: BEAD_LANE_TITLES[id] }),
16
+ );
17
+
18
+ export type BeadsFilter = "all" | "high_priority" | "assigned";
19
+
20
+ export interface BeadsViewOptions {
21
+ query?: string;
22
+ filter?: BeadsFilter;
23
+ }
24
+
25
+ export type BeadLanes = Record<BeadLane, BeadSummary[]>;
26
+ export type BeadLaneCounts = Record<BeadLane, number>;
27
+
28
+ export interface BeadsView {
29
+ lanes: BeadLanes;
30
+ counts: BeadLaneCounts;
31
+ }
32
+
33
+ export interface BeadSection {
34
+ lane: BeadLane;
35
+ title: string;
36
+ data: BeadSummary[];
37
+ }
38
+
39
+ export function buildBeadSections(view: BeadsView, activeFiltering: boolean): BeadSection[] {
40
+ const sections: BeadSection[] = [];
41
+
42
+ for (const lane of BEAD_LANES) {
43
+ const data = view.lanes[lane];
44
+ if (activeFiltering && data.length === 0) continue;
45
+
46
+ sections.push({
47
+ lane,
48
+ title: lane === "ready" ? "Ready frontier" : BEAD_LANE_TITLES[lane],
49
+ data,
50
+ });
51
+ }
52
+
53
+ return sections;
54
+ }
55
+
56
+ export function issueAccessibilityLabel(issue: BeadSummary, lane: BeadLane): string {
57
+ const laneDescription = lane === "ready" ? "Ready frontier" : `${BEAD_LANE_TITLES[lane]} lane`;
58
+ const assigneeDescription = issue.assignee ? `Assigned to ${issue.assignee}` : "Unassigned";
59
+ const activity: string[] = [];
60
+
61
+ if (issue.dependencyCount > 0) {
62
+ activity.push(
63
+ `${issue.dependencyCount} ${issue.dependencyCount === 1 ? "dependency" : "dependencies"}`,
64
+ );
65
+ }
66
+ if (issue.dependentCount > 0) {
67
+ activity.push(
68
+ `${issue.dependentCount} ${issue.dependentCount === 1 ? "dependent" : "dependents"}`,
69
+ );
70
+ }
71
+ if (issue.commentCount > 0) {
72
+ activity.push(`${issue.commentCount} ${issue.commentCount === 1 ? "comment" : "comments"}`);
73
+ }
74
+
75
+ const descriptions = [
76
+ `Open issue ${issue.id}: ${issue.title}`,
77
+ `Priority P${issue.priority}`,
78
+ laneDescription,
79
+ `Issue type ${issue.issueType}`,
80
+ assigneeDescription,
81
+ ...activity,
82
+ ];
83
+
84
+ return `${descriptions.join(". ")}.`;
85
+ }
86
+
87
+ export function beadLaneFor(issue: BeadSummary): BeadLane {
88
+ if (issue.status === "in_progress" || issue.status === "hooked") return "in_progress";
89
+ if (issue.isBlocked) return "blocked";
90
+ if (issue.isReady) return "ready";
91
+ return "other";
92
+ }
93
+
94
+ function matchesSearch(issue: BeadSummary, query: string): boolean {
95
+ if (!query) return true;
96
+ if (issue.id.toLowerCase().includes(query)) return true;
97
+ if (issue.title.toLowerCase().includes(query)) return true;
98
+ if (issue.assignee?.toLowerCase().includes(query)) return true;
99
+ return issue.labels.some((label) => label.toLowerCase().includes(query));
100
+ }
101
+
102
+ function matchesFilter(issue: BeadSummary, filter: BeadsFilter): boolean {
103
+ if (filter === "high_priority") return issue.priority <= 1;
104
+ if (filter === "assigned") return issue.assignee !== null;
105
+ return true;
106
+ }
107
+
108
+ function updatedTimestamp(updatedAt: string | null): number {
109
+ if (updatedAt === null) return Number.NEGATIVE_INFINITY;
110
+ const timestamp = Date.parse(updatedAt);
111
+ return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
112
+ }
113
+
114
+ function compareBeads(left: BeadSummary, right: BeadSummary): number {
115
+ const byPriority = left.priority - right.priority;
116
+ if (byPriority !== 0) return byPriority;
117
+
118
+ const leftUpdatedAt = updatedTimestamp(left.updatedAt);
119
+ const rightUpdatedAt = updatedTimestamp(right.updatedAt);
120
+ if (leftUpdatedAt !== rightUpdatedAt) return rightUpdatedAt > leftUpdatedAt ? 1 : -1;
121
+
122
+ if (left.id < right.id) return -1;
123
+ if (left.id > right.id) return 1;
124
+ return 0;
125
+ }
126
+
127
+ export function buildBeadsView(
128
+ issues: readonly BeadSummary[],
129
+ options: BeadsViewOptions = {},
130
+ ): BeadsView {
131
+ const lanes: BeadLanes = {
132
+ ready: [],
133
+ in_progress: [],
134
+ blocked: [],
135
+ other: [],
136
+ };
137
+ const counts: BeadLaneCounts = {
138
+ ready: 0,
139
+ in_progress: 0,
140
+ blocked: 0,
141
+ other: 0,
142
+ };
143
+ const query = options.query?.trim().toLowerCase() ?? "";
144
+ const filter = options.filter ?? "all";
145
+
146
+ for (const issue of issues) {
147
+ const lane = beadLaneFor(issue);
148
+ counts[lane] += 1;
149
+ if (matchesFilter(issue, filter) && matchesSearch(issue, query)) lanes[lane].push(issue);
150
+ }
151
+
152
+ for (const lane of BEAD_LANES) lanes[lane].sort(compareBeads);
153
+
154
+ return { lanes, counts };
155
+ }