Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ALTER TABLE repos ADD COLUMN IF NOT EXISTS security_policy_enabled boolean;
ALTER TABLE repos ADD COLUMN IF NOT EXISTS security_file_enabled boolean;
ALTER TABLE repos ADD COLUMN IF NOT EXISTS snapshot_at timestamptz;
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
mbani01 marked this conversation as resolved.

CREATE TABLE IF NOT EXISTS repo_activity_snapshot (
repo_id bigint PRIMARY KEY REFERENCES repos(id) ON DELETE CASCADE,
snapshot_at timestamptz NOT NULL,
window_months int NOT NULL DEFAULT 12,
-- commit activity
commits_last_12m int,
commits_last_6m int,
commits_prior_6m int,
-- PR health
prs_opened_last_12m int,
prs_merged_last_12m int,
prs_closed_unmerged_12m int,
pr_median_time_to_merge_hours int,
pr_median_time_to_first_response_hours int,
-- issue health
issues_opened_last_12m int,
issues_closed_last_12m int,
issues_opened_last_6m int,
issues_opened_prior_6m int,
issues_open_now int,
issue_median_time_to_close_hours int,
issue_median_time_to_first_response_hours int
);

CREATE INDEX IF NOT EXISTS repo_activity_snapshot_snapshot_at_idx
ON repo_activity_snapshot (snapshot_at);
105 changes: 105 additions & 0 deletions services/apps/packages_worker/src/enricher/computeMedians.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const MS_PER_HOUR = 1000 * 60 * 60

function median(values: number[]): number | null {
if (values.length === 0) return null
const sorted = [...values].sort((a, b) => a - b)
const middleIndex = Math.floor(sorted.length / 2)
return sorted.length % 2 === 0
? (sorted[middleIndex - 1] + sorted[middleIndex]) / 2
: sorted[middleIndex]
}

function hoursBetween(startDate: string, endDate: string): number {
return (new Date(endDate).getTime() - new Date(startDate).getTime()) / MS_PER_HOUR
}

function toIntHours(hours: number | null): number | null {
return hours != null ? Math.round(hours) : null
}

// Shapes mirror exactly what GitHub GraphQL returns for comments/reviews nodes
export interface ResponseNode {
createdAt: string
author: { login: string } | null
}

export interface PrNode {
createdAt: string
mergedAt: string | null
author: { login: string } | null
comments: { nodes: ResponseNode[] }
reviews: { nodes: ResponseNode[] }
}

export interface IssueNode {
createdAt: string
closedAt: string | null
author: { login: string } | null
comments: { nodes: ResponseNode[] }
}

function firstNonAuthorResponseHours(
itemCreatedAt: string,
authorLogin: string | null,
responses: ResponseNode[],
): number | null {
const firstResponse = responses.find(
(response) => response.author?.login && response.author.login !== authorLogin,
)
return firstResponse ? hoursBetween(itemCreatedAt, firstResponse.createdAt) : null
}

export function computePrMedians(prs: PrNode[]): {
medianTimeToMergeHours: number | null
medianTimeToFirstResponseHours: number | null
} {
const mergeHours: number[] = []
const firstResponseHours: number[] = []

for (const pr of prs) {
if (pr.mergedAt != null) {
mergeHours.push(hoursBetween(pr.createdAt, pr.mergedAt))
}

const allResponses = [...pr.comments.nodes, ...pr.reviews.nodes].sort(
(left, right) => new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime(),
)
const responseHours = firstNonAuthorResponseHours(
pr.createdAt,
pr.author?.login ?? null,
allResponses,
)
if (responseHours != null) firstResponseHours.push(responseHours)
}

return {
medianTimeToMergeHours: toIntHours(median(mergeHours)),
medianTimeToFirstResponseHours: toIntHours(median(firstResponseHours)),
}
}

export function computeIssueMedians(issues: IssueNode[]): {
medianTimeToCloseHours: number | null
medianTimeToFirstResponseHours: number | null
} {
const closeHours: number[] = []
const firstResponseHours: number[] = []

for (const issue of issues) {
if (issue.closedAt != null) {
closeHours.push(hoursBetween(issue.createdAt, issue.closedAt))
}

const responseHours = firstNonAuthorResponseHours(
issue.createdAt,
issue.author?.login ?? null,
issue.comments.nodes,
)
if (responseHours != null) firstResponseHours.push(responseHours)
}

return {
medianTimeToCloseHours: toIntHours(median(closeHours)),
medianTimeToFirstResponseHours: toIntHours(median(firstResponseHours)),
}
}
Loading
Loading