Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

repo-pulse.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

repo-pulse.tsBlame286 lines · 1 contributor
7c975c6Claude1/**
2 * Block J18 — Repository pulse / activity summary.
3 *
4 * Pure rollups for the `/:owner/:repo/pulse` page. Takes already-fetched
5 * commits + PR + issue rows and buckets them into a time window. No I/O —
6 * the route handler is responsible for querying git + Drizzle.
7 *
8 * A "pulse" is a recent-activity snapshot over a rolling window (1d / 7d /
9 * 30d / 90d). It answers: "who's been pushing, what's moving, what's new
10 * and what's closing?"
11 */
12
13export const PULSE_WINDOWS = ["1d", "7d", "30d", "90d"] as const;
14export type PulseWindow = (typeof PULSE_WINDOWS)[number];
15export const DEFAULT_WINDOW: PulseWindow = "7d";
16
17const WINDOW_DAYS: Record<PulseWindow, number> = {
18 "1d": 1,
19 "7d": 7,
20 "30d": 30,
21 "90d": 90,
22};
23
24/** Pure: validate a raw string as a supported pulse window, else fall back. */
25export function parseWindow(raw: unknown): PulseWindow {
26 if (typeof raw === "string" && (PULSE_WINDOWS as readonly string[]).includes(raw)) {
27 return raw as PulseWindow;
28 }
29 return DEFAULT_WINDOW;
30}
31
32/** Pure: return the Date at the start of the window relative to `now`. */
33export function windowStart(now: Date, w: PulseWindow): Date {
34 const days = WINDOW_DAYS[w];
35 const d = new Date(now.getTime());
36 d.setUTCDate(d.getUTCDate() - days);
37 return d;
38}
39
40/** Pure: number of days represented by a given pulse window. */
41export function windowDays(w: PulseWindow): number {
42 return WINDOW_DAYS[w];
43}
44
45function toMs(d: string | Date | null | undefined): number | null {
46 if (d === null || d === undefined) return null;
47 if (d instanceof Date) {
48 const t = d.getTime();
49 return Number.isFinite(t) ? t : null;
50 }
51 const t = Date.parse(d);
52 return Number.isFinite(t) ? t : null;
53}
54
55function inWindow(t: number | null, start: Date, end: Date): boolean {
56 if (t === null) return false;
57 return t >= start.getTime() && t <= end.getTime();
58}
59
60// ---------------------------------------------------------------------------
61// Commits
62// ---------------------------------------------------------------------------
63
64export interface PulseCommit {
65 sha: string;
66 author: string;
67 authorEmail: string;
68 date: string;
69 message?: string;
70}
71
72export interface ContributorCount {
73 author: string;
74 email: string;
75 count: number;
76}
77
78export interface CommitPulse {
79 total: number;
80 byAuthor: ContributorCount[];
81 firstSha: string | null;
82 lastSha: string | null;
83}
84
85/**
86 * Pure: count commits inside [start, end] and group by author email.
87 * `commits` is the newest-first list returned by `listCommits`.
88 */
89export function summariseCommits(
90 commits: PulseCommit[],
91 start: Date,
92 end: Date
93): CommitPulse {
94 const inRange = commits.filter((c) => inWindow(toMs(c.date), start, end));
95 const counts = new Map<string, ContributorCount>();
96 for (const c of inRange) {
97 const emailKey = (c.authorEmail || "").toLowerCase().trim();
98 const nameKey = (c.author || "").toLowerCase().trim();
99 const key = emailKey || nameKey || "(unknown)";
100 const prev = counts.get(key);
101 if (prev) {
102 prev.count += 1;
103 } else {
104 counts.set(key, {
105 author: c.author || "(unknown)",
106 email: c.authorEmail || "",
107 count: 1,
108 });
109 }
110 }
111 const byAuthor = Array.from(counts.values()).sort(
112 (a, b) => b.count - a.count || a.author.localeCompare(b.author)
113 );
114 return {
115 total: inRange.length,
116 byAuthor,
117 firstSha: inRange.length ? inRange[inRange.length - 1].sha : null,
118 lastSha: inRange.length ? inRange[0].sha : null,
119 };
120}
121
122// ---------------------------------------------------------------------------
123// Pull requests
124// ---------------------------------------------------------------------------
125
126export interface PulsePr {
127 id?: string;
128 number: number;
129 title: string;
130 state: string; // "open" | "closed" | "merged"
131 isDraft?: boolean;
132 authorName?: string;
133 createdAt: string | Date;
134 updatedAt: string | Date;
135 closedAt: string | Date | null;
136 mergedAt: string | Date | null;
137}
138
139export interface PrPulse {
140 opened: number;
141 mergedCount: number;
142 closed: number;
143 active: number;
144 openedList: PulsePr[];
145 mergedList: PulsePr[];
146}
147
148/**
149 * Pure: bucket PRs by what changed in-window.
150 * - `opened`: createdAt in window
151 * - `mergedCount`: mergedAt in window (mutually exclusive with closed)
152 * - `closed`: closedAt in window AND not merged in window
153 * - `active`: state='open' and updatedAt in window
154 */
155export function summarisePrs(prs: PulsePr[], start: Date, end: Date): PrPulse {
156 let opened = 0,
157 mergedCount = 0,
158 closed = 0,
159 active = 0;
160 const openedList: PulsePr[] = [];
161 const mergedList: PulsePr[] = [];
162 for (const p of prs) {
163 const created = toMs(p.createdAt);
164 const closedMs = toMs(p.closedAt);
165 const mergedMs = toMs(p.mergedAt);
166 const updated = toMs(p.updatedAt);
167 const createdIn = inWindow(created, start, end);
168 const mergedIn = inWindow(mergedMs, start, end);
169 const closedIn = inWindow(closedMs, start, end);
170 if (createdIn) {
171 opened++;
172 openedList.push(p);
173 }
174 if (mergedIn) {
175 mergedCount++;
176 mergedList.push(p);
177 } else if (closedIn) {
178 closed++;
179 }
180 if (p.state === "open" && inWindow(updated, start, end)) active++;
181 }
182 return { opened, mergedCount, closed, active, openedList, mergedList };
183}
184
185// ---------------------------------------------------------------------------
186// Issues
187// ---------------------------------------------------------------------------
188
189export interface PulseIssue {
190 id?: string;
191 number: number;
192 title: string;
193 state: string; // "open" | "closed"
194 authorName?: string;
195 createdAt: string | Date;
196 updatedAt: string | Date;
197 closedAt: string | Date | null;
198}
199
200export interface IssuePulse {
201 opened: number;
202 closed: number;
203 active: number;
204 openedList: PulseIssue[];
205 closedList: PulseIssue[];
206}
207
208/**
209 * Pure: bucket issues into opened/closed/active counts over the window.
210 * - `opened`: createdAt in window
211 * - `closed`: closedAt in window
212 * - `active`: state='open' AND updatedAt in window
213 */
214export function summariseIssues(
215 issues: PulseIssue[],
216 start: Date,
217 end: Date
218): IssuePulse {
219 let opened = 0,
220 closed = 0,
221 active = 0;
222 const openedList: PulseIssue[] = [];
223 const closedList: PulseIssue[] = [];
224 for (const i of issues) {
225 const created = toMs(i.createdAt);
226 const closedMs = toMs(i.closedAt);
227 const updated = toMs(i.updatedAt);
228 if (inWindow(created, start, end)) {
229 opened++;
230 openedList.push(i);
231 }
232 if (inWindow(closedMs, start, end)) {
233 closed++;
234 closedList.push(i);
235 }
236 if (i.state === "open" && inWindow(updated, start, end)) active++;
237 }
238 return { opened, closed, active, openedList, closedList };
239}
240
241// ---------------------------------------------------------------------------
242// One-shot builder
243// ---------------------------------------------------------------------------
244
245export interface PulseReport {
246 window: PulseWindow;
247 days: number;
248 start: string;
249 end: string;
250 commits: CommitPulse;
251 prs: PrPulse;
252 issues: IssuePulse;
253}
254
255export function buildPulseReport(opts: {
256 window: PulseWindow;
257 now: Date;
258 commits: PulseCommit[];
259 prs: PulsePr[];
260 issues: PulseIssue[];
261}): PulseReport {
262 const start = windowStart(opts.now, opts.window);
263 const end = opts.now;
264 return {
265 window: opts.window,
266 days: windowDays(opts.window),
267 start: start.toISOString(),
268 end: end.toISOString(),
269 commits: summariseCommits(opts.commits, start, end),
270 prs: summarisePrs(opts.prs, start, end),
271 issues: summariseIssues(opts.issues, start, end),
272 };
273}
274
275export const __internal = {
276 PULSE_WINDOWS,
277 DEFAULT_WINDOW,
278 WINDOW_DAYS,
279 parseWindow,
280 windowStart,
281 windowDays,
282 summariseCommits,
283 summarisePrs,
284 summariseIssues,
285 buildPulseReport,
286};