Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

54 linesCodeBlameRaw
1/**
2 * How active a project is lately, for sorting a workspace's projects: each
3 * push, work item or pull request opened or closed, review, comment and
4 * deployment adds one (a work item's, to every repository it is linked to), and what came before counts for half as much every
5 * week. Kept as a score and the time it was last raised, so raising it is
6 * one row's update. Pure, so it is tested apart from the service.
7 */
8
9import type { EventType } from "@g1t/contracts";
10
11/** What happens to a project's code and work that counts as activity. */
12export const ACTIVE: ReadonlySet<EventType> = new Set<EventType>([
13 "git.push",
14 "work_item.opened",
15 "work_item.closed",
16 "work_item.reopened",
17 "pull.opened",
18 "pull.updated",
19 "pull.merged",
20 "pull.closed",
21 "review.completed",
22 "comment.created",
23 "deployment.succeeded",
24 "deployment.failed",
25]);
26
27/**
28 * The repositories an event's activity counts for: every repository a work
29 * item (or a comment on one) is linked to, else the one it names.
30 */
31export function activeRepos(repoId: string | null | undefined, data: unknown): string[] {
32 const linked = (data as { repoIds?: unknown } | null)?.repoIds;
33 if (Array.isArray(linked) && linked.length > 0) return linked.filter((id): id is string => typeof id === "string" && id !== "");
34 const named = repoId ?? (data as { repoId?: unknown } | null)?.repoId;
35 return typeof named === "string" && named !== "" ? [named] : [];
36}
37
38/** How long until activity counts for half. */
39export const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;
40
41/** The score `score` raised at `lastAt` is worth at `at` (RFC 3339 times). */
42export function decayed(score: number, lastAt: string | null, at: string): number {
43 if (!lastAt || !(score > 0)) return 0;
44 const elapsed = Date.parse(at) - Date.parse(lastAt);
45 if (!Number.isFinite(elapsed)) return 0;
46 // Events delivered out of order count in full: never worth more than they were.
47 return score * Math.pow(0.5, Math.max(0, elapsed) / HALF_LIFE_MS);
48}
49
50/** The score and its time after one more piece of activity at `at`. */
51export function raised(score: number, lastAt: string | null, at: string): { score: number; at: string } {
52 const later = !lastAt || Date.parse(at) >= Date.parse(lastAt) ? at : lastAt;
53 return { score: decayed(score, lastAt, later) + 1, at: later };
54}