@0xbigboss/gh-pulse-core 1.0.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.
Files changed (73) hide show
  1. package/dist/cache.cjs +312 -0
  2. package/dist/cache.d.cts +58 -0
  3. package/dist/cache.d.ts +58 -0
  4. package/dist/cache.js +303 -0
  5. package/dist/config.cjs +182 -0
  6. package/dist/config.d.cts +248 -0
  7. package/dist/config.d.ts +248 -0
  8. package/dist/config.js +172 -0
  9. package/dist/filters.cjs +16 -0
  10. package/dist/filters.d.cts +3 -0
  11. package/dist/filters.d.ts +3 -0
  12. package/dist/filters.js +12 -0
  13. package/dist/github.cjs +240 -0
  14. package/dist/github.d.cts +46 -0
  15. package/dist/github.d.ts +46 -0
  16. package/dist/github.js +235 -0
  17. package/dist/index.cjs +28 -0
  18. package/dist/index.d.cts +11 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.js +11 -0
  21. package/dist/reports/context.cjs +8 -0
  22. package/dist/reports/context.d.cts +7 -0
  23. package/dist/reports/context.d.ts +7 -0
  24. package/dist/reports/context.js +5 -0
  25. package/dist/reports/exec.cjs +160 -0
  26. package/dist/reports/exec.d.cts +6 -0
  27. package/dist/reports/exec.d.ts +6 -0
  28. package/dist/reports/exec.js +157 -0
  29. package/dist/reports/index.cjs +21 -0
  30. package/dist/reports/index.d.cts +5 -0
  31. package/dist/reports/index.d.ts +5 -0
  32. package/dist/reports/index.js +5 -0
  33. package/dist/reports/meta.cjs +15 -0
  34. package/dist/reports/meta.d.cts +12 -0
  35. package/dist/reports/meta.d.ts +12 -0
  36. package/dist/reports/meta.js +12 -0
  37. package/dist/reports/personal.cjs +90 -0
  38. package/dist/reports/personal.d.cts +8 -0
  39. package/dist/reports/personal.d.ts +8 -0
  40. package/dist/reports/personal.js +87 -0
  41. package/dist/reports/team.cjs +127 -0
  42. package/dist/reports/team.d.cts +6 -0
  43. package/dist/reports/team.d.ts +6 -0
  44. package/dist/reports/team.js +124 -0
  45. package/dist/reports/types.cjs +2 -0
  46. package/dist/reports/types.d.cts +144 -0
  47. package/dist/reports/types.d.ts +144 -0
  48. package/dist/reports/types.js +1 -0
  49. package/dist/reports/utils.cjs +71 -0
  50. package/dist/reports/utils.d.cts +6 -0
  51. package/dist/reports/utils.d.ts +6 -0
  52. package/dist/reports/utils.js +65 -0
  53. package/dist/repos.cjs +102 -0
  54. package/dist/repos.d.cts +12 -0
  55. package/dist/repos.d.ts +12 -0
  56. package/dist/repos.js +96 -0
  57. package/dist/sync.cjs +360 -0
  58. package/dist/sync.d.cts +24 -0
  59. package/dist/sync.d.ts +24 -0
  60. package/dist/sync.js +357 -0
  61. package/dist/team.cjs +45 -0
  62. package/dist/team.d.cts +10 -0
  63. package/dist/team.d.ts +10 -0
  64. package/dist/team.js +42 -0
  65. package/dist/time.cjs +153 -0
  66. package/dist/time.d.cts +13 -0
  67. package/dist/time.d.ts +13 -0
  68. package/dist/time.js +145 -0
  69. package/dist/types.cjs +2 -0
  70. package/dist/types.d.cts +133 -0
  71. package/dist/types.d.ts +133 -0
  72. package/dist/types.js +1 -0
  73. package/package.json +29 -0
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTeamReport = buildTeamReport;
4
+ const utils_1 = require("./utils.cjs");
5
+ const context_1 = require("./context.cjs");
6
+ const filters_1 = require("../filters.cjs");
7
+ function buildTeamReport(cache, options) {
8
+ const { pullRequests, commits } = (0, context_1.loadReportData)(cache, options.repos);
9
+ const now = options.meta.generated_at;
10
+ const exclude = options.excludeAuthors;
11
+ const teamSet = new Set(options.teamMembers);
12
+ const prLookup = buildPullRequestLookup(pullRequests);
13
+ const events = cache
14
+ .getEvents({ since: options.timeRange.start, until: options.timeRange.end })
15
+ .filter((event) => options.repos.includes(event.repo));
16
+ const prEvents = events.filter((event) => 'pr_number' in event);
17
+ const reviewEvents = events.filter((event) => event.type === 'review_submitted');
18
+ const opened = prEvents.filter((event) => event.type === 'pr_opened' &&
19
+ teamHasAuthor(teamSet, event.author) &&
20
+ !(0, filters_1.isExcludedAuthor)(event.author, exclude, options.excludeBots));
21
+ const merged = prEvents.filter((event) => event.type === 'pr_merged' &&
22
+ prAuthorInTeam(prLookup, teamSet, event.repo, event.pr_number, exclude, options.excludeBots));
23
+ const closed = prEvents.filter((event) => event.type === 'pr_closed' &&
24
+ prAuthorInTeam(prLookup, teamSet, event.repo, event.pr_number, exclude, options.excludeBots));
25
+ const mergedPrs = merged
26
+ .map((event) => prLookup.get(`${event.repo}#${event.pr_number}`))
27
+ .filter((pr) => Boolean(pr))
28
+ .filter((pr) => options.includeDrafts || !pr.draft);
29
+ const cycleTimes = mergedPrs.map((pr) => {
30
+ const prEventsForPr = cache.getEventsForPr(pr.repo, pr.number);
31
+ const metrics = (0, utils_1.computeCycleTime)(pr, prEventsForPr);
32
+ return { pr: (0, utils_1.buildPullRequestSummary)(pr, now), metrics };
33
+ });
34
+ const cycleTimeValues = cycleTimes
35
+ .map((entry) => entry.metrics?.total_time)
36
+ .filter((value) => typeof value === 'number' && value > 0);
37
+ const reviewValues = cycleTimes
38
+ .map((entry) => entry.metrics?.review_time)
39
+ .filter((value) => typeof value === 'number' && value >= 0);
40
+ const reviewEngagement = mergedPrs.map((pr) => {
41
+ const prEventsForPr = cache.getEventsForPr(pr.repo, pr.number);
42
+ const metrics = (0, utils_1.computeReviewEngagement)(pr, prEventsForPr);
43
+ return { pr: (0, utils_1.buildPullRequestSummary)(pr, now), metrics };
44
+ });
45
+ const memberBreakdown = buildMemberBreakdown(options.teamMembers, pullRequests, commits, reviewEvents, exclude, options.excludeBots, options.timeRange.start, options.timeRange.end);
46
+ const blockers = findBlockers(pullRequests, options.includeDrafts, exclude, options.excludeBots, options.thresholds, now);
47
+ return {
48
+ type: 'team',
49
+ meta: options.meta,
50
+ team_size: options.teamMembers.length,
51
+ velocity: {
52
+ prs_opened: opened.length,
53
+ prs_merged: merged.length,
54
+ prs_closed: closed.length,
55
+ cycle_time_p50: (0, utils_1.percentile)(cycleTimeValues, 50),
56
+ cycle_time_p90: (0, utils_1.percentile)(cycleTimeValues, 90),
57
+ review_turnaround_p50: (0, utils_1.percentile)(reviewValues, 50),
58
+ review_turnaround_p90: (0, utils_1.percentile)(reviewValues, 90),
59
+ },
60
+ members: memberBreakdown,
61
+ blockers,
62
+ cycle_times: cycleTimes,
63
+ review_engagement: reviewEngagement,
64
+ };
65
+ }
66
+ function buildMemberBreakdown(teamMembers, pullRequests, commits, reviewEvents, exclude, excludeBots, since, until) {
67
+ return [...teamMembers]
68
+ .filter((member) => !(0, filters_1.isExcludedAuthor)(member, exclude, excludeBots))
69
+ .map((member) => {
70
+ const authored = pullRequests.filter((pr) => pr.author === member &&
71
+ pr.created_at >= since &&
72
+ pr.created_at <= until &&
73
+ !(0, filters_1.isExcludedAuthor)(pr.author, exclude, excludeBots));
74
+ const reviewed = reviewEvents.filter((event) => event.type === 'review_submitted' &&
75
+ event.reviewer === member &&
76
+ !(0, filters_1.isExcludedAuthor)(event.reviewer, exclude, excludeBots));
77
+ const memberCommits = commits.filter((commit) => commit.author === member &&
78
+ commit.committed_at >= since &&
79
+ commit.committed_at <= until &&
80
+ !(0, filters_1.isExcludedAuthor)(commit.author, exclude, excludeBots));
81
+ const linesChanged = memberCommits.reduce((total, commit) => total + commit.additions + commit.deletions, 0);
82
+ return {
83
+ user: member,
84
+ prs_authored: authored.length,
85
+ prs_reviewed: reviewed.length,
86
+ commits: memberCommits.length,
87
+ lines_changed: linesChanged,
88
+ };
89
+ })
90
+ .toSorted((a, b) => a.user.localeCompare(b.user));
91
+ }
92
+ function findBlockers(pullRequests, includeDrafts, exclude, excludeBots, thresholds, now) {
93
+ return pullRequests
94
+ .filter((pr) => pr.state === 'open')
95
+ .filter((pr) => includeDrafts || !pr.draft)
96
+ .filter((pr) => !(0, filters_1.isExcludedAuthor)(pr.author, exclude, excludeBots))
97
+ .flatMap((pr) => {
98
+ const blockers = [];
99
+ const summary = (0, utils_1.buildPullRequestSummary)(pr, now);
100
+ const staleDays = diffDays(pr.updated_at, now);
101
+ const stuckDays = diffDays(pr.created_at, now);
102
+ if (staleDays >= thresholds.stale_days) {
103
+ blockers.push({ pr: summary, status: 'stale', days: staleDays });
104
+ }
105
+ if (stuckDays >= thresholds.stuck_days) {
106
+ blockers.push({ pr: summary, status: 'stuck', days: stuckDays });
107
+ }
108
+ return blockers;
109
+ })
110
+ .toSorted((a, b) => b.days - a.days);
111
+ }
112
+ function diffDays(start, end) {
113
+ return Math.floor((end - start) / (24 * 60 * 60 * 1000));
114
+ }
115
+ function teamHasAuthor(team, author) {
116
+ return team.has(author);
117
+ }
118
+ function prAuthorInTeam(lookup, team, repo, number, exclude, excludeBots) {
119
+ const pr = lookup.get(`${repo}#${number}`);
120
+ if (!pr || (0, filters_1.isExcludedAuthor)(pr.author, exclude, excludeBots)) {
121
+ return false;
122
+ }
123
+ return teamHasAuthor(team, pr.author);
124
+ }
125
+ function buildPullRequestLookup(pullRequests) {
126
+ return new Map(pullRequests.map((pr) => [`${pr.repo}#${pr.number}`, pr]));
127
+ }
@@ -0,0 +1,6 @@
1
+ import type { Cache } from "../cache.cjs";
2
+ import type { ReportInputs, TeamReport } from "./types.cjs";
3
+ export interface TeamReportOptions extends ReportInputs {
4
+ meta: TeamReport['meta'];
5
+ }
6
+ export declare function buildTeamReport(cache: Cache, options: TeamReportOptions): TeamReport;
@@ -0,0 +1,6 @@
1
+ import type { Cache } from "../cache.js";
2
+ import type { ReportInputs, TeamReport } from "./types.js";
3
+ export interface TeamReportOptions extends ReportInputs {
4
+ meta: TeamReport['meta'];
5
+ }
6
+ export declare function buildTeamReport(cache: Cache, options: TeamReportOptions): TeamReport;
@@ -0,0 +1,124 @@
1
+ import { buildPullRequestSummary, computeCycleTime, computeReviewEngagement, percentile, } from "./utils.js";
2
+ import { loadReportData } from "./context.js";
3
+ import { isExcludedAuthor } from "../filters.js";
4
+ export function buildTeamReport(cache, options) {
5
+ const { pullRequests, commits } = loadReportData(cache, options.repos);
6
+ const now = options.meta.generated_at;
7
+ const exclude = options.excludeAuthors;
8
+ const teamSet = new Set(options.teamMembers);
9
+ const prLookup = buildPullRequestLookup(pullRequests);
10
+ const events = cache
11
+ .getEvents({ since: options.timeRange.start, until: options.timeRange.end })
12
+ .filter((event) => options.repos.includes(event.repo));
13
+ const prEvents = events.filter((event) => 'pr_number' in event);
14
+ const reviewEvents = events.filter((event) => event.type === 'review_submitted');
15
+ const opened = prEvents.filter((event) => event.type === 'pr_opened' &&
16
+ teamHasAuthor(teamSet, event.author) &&
17
+ !isExcludedAuthor(event.author, exclude, options.excludeBots));
18
+ const merged = prEvents.filter((event) => event.type === 'pr_merged' &&
19
+ prAuthorInTeam(prLookup, teamSet, event.repo, event.pr_number, exclude, options.excludeBots));
20
+ const closed = prEvents.filter((event) => event.type === 'pr_closed' &&
21
+ prAuthorInTeam(prLookup, teamSet, event.repo, event.pr_number, exclude, options.excludeBots));
22
+ const mergedPrs = merged
23
+ .map((event) => prLookup.get(`${event.repo}#${event.pr_number}`))
24
+ .filter((pr) => Boolean(pr))
25
+ .filter((pr) => options.includeDrafts || !pr.draft);
26
+ const cycleTimes = mergedPrs.map((pr) => {
27
+ const prEventsForPr = cache.getEventsForPr(pr.repo, pr.number);
28
+ const metrics = computeCycleTime(pr, prEventsForPr);
29
+ return { pr: buildPullRequestSummary(pr, now), metrics };
30
+ });
31
+ const cycleTimeValues = cycleTimes
32
+ .map((entry) => entry.metrics?.total_time)
33
+ .filter((value) => typeof value === 'number' && value > 0);
34
+ const reviewValues = cycleTimes
35
+ .map((entry) => entry.metrics?.review_time)
36
+ .filter((value) => typeof value === 'number' && value >= 0);
37
+ const reviewEngagement = mergedPrs.map((pr) => {
38
+ const prEventsForPr = cache.getEventsForPr(pr.repo, pr.number);
39
+ const metrics = computeReviewEngagement(pr, prEventsForPr);
40
+ return { pr: buildPullRequestSummary(pr, now), metrics };
41
+ });
42
+ const memberBreakdown = buildMemberBreakdown(options.teamMembers, pullRequests, commits, reviewEvents, exclude, options.excludeBots, options.timeRange.start, options.timeRange.end);
43
+ const blockers = findBlockers(pullRequests, options.includeDrafts, exclude, options.excludeBots, options.thresholds, now);
44
+ return {
45
+ type: 'team',
46
+ meta: options.meta,
47
+ team_size: options.teamMembers.length,
48
+ velocity: {
49
+ prs_opened: opened.length,
50
+ prs_merged: merged.length,
51
+ prs_closed: closed.length,
52
+ cycle_time_p50: percentile(cycleTimeValues, 50),
53
+ cycle_time_p90: percentile(cycleTimeValues, 90),
54
+ review_turnaround_p50: percentile(reviewValues, 50),
55
+ review_turnaround_p90: percentile(reviewValues, 90),
56
+ },
57
+ members: memberBreakdown,
58
+ blockers,
59
+ cycle_times: cycleTimes,
60
+ review_engagement: reviewEngagement,
61
+ };
62
+ }
63
+ function buildMemberBreakdown(teamMembers, pullRequests, commits, reviewEvents, exclude, excludeBots, since, until) {
64
+ return [...teamMembers]
65
+ .filter((member) => !isExcludedAuthor(member, exclude, excludeBots))
66
+ .map((member) => {
67
+ const authored = pullRequests.filter((pr) => pr.author === member &&
68
+ pr.created_at >= since &&
69
+ pr.created_at <= until &&
70
+ !isExcludedAuthor(pr.author, exclude, excludeBots));
71
+ const reviewed = reviewEvents.filter((event) => event.type === 'review_submitted' &&
72
+ event.reviewer === member &&
73
+ !isExcludedAuthor(event.reviewer, exclude, excludeBots));
74
+ const memberCommits = commits.filter((commit) => commit.author === member &&
75
+ commit.committed_at >= since &&
76
+ commit.committed_at <= until &&
77
+ !isExcludedAuthor(commit.author, exclude, excludeBots));
78
+ const linesChanged = memberCommits.reduce((total, commit) => total + commit.additions + commit.deletions, 0);
79
+ return {
80
+ user: member,
81
+ prs_authored: authored.length,
82
+ prs_reviewed: reviewed.length,
83
+ commits: memberCommits.length,
84
+ lines_changed: linesChanged,
85
+ };
86
+ })
87
+ .toSorted((a, b) => a.user.localeCompare(b.user));
88
+ }
89
+ function findBlockers(pullRequests, includeDrafts, exclude, excludeBots, thresholds, now) {
90
+ return pullRequests
91
+ .filter((pr) => pr.state === 'open')
92
+ .filter((pr) => includeDrafts || !pr.draft)
93
+ .filter((pr) => !isExcludedAuthor(pr.author, exclude, excludeBots))
94
+ .flatMap((pr) => {
95
+ const blockers = [];
96
+ const summary = buildPullRequestSummary(pr, now);
97
+ const staleDays = diffDays(pr.updated_at, now);
98
+ const stuckDays = diffDays(pr.created_at, now);
99
+ if (staleDays >= thresholds.stale_days) {
100
+ blockers.push({ pr: summary, status: 'stale', days: staleDays });
101
+ }
102
+ if (stuckDays >= thresholds.stuck_days) {
103
+ blockers.push({ pr: summary, status: 'stuck', days: stuckDays });
104
+ }
105
+ return blockers;
106
+ })
107
+ .toSorted((a, b) => b.days - a.days);
108
+ }
109
+ function diffDays(start, end) {
110
+ return Math.floor((end - start) / (24 * 60 * 60 * 1000));
111
+ }
112
+ function teamHasAuthor(team, author) {
113
+ return team.has(author);
114
+ }
115
+ function prAuthorInTeam(lookup, team, repo, number, exclude, excludeBots) {
116
+ const pr = lookup.get(`${repo}#${number}`);
117
+ if (!pr || isExcludedAuthor(pr.author, exclude, excludeBots)) {
118
+ return false;
119
+ }
120
+ return teamHasAuthor(team, pr.author);
121
+ }
122
+ function buildPullRequestLookup(pullRequests) {
123
+ return new Map(pullRequests.map((pr) => [`${pr.repo}#${pr.number}`, pr]));
124
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,144 @@
1
+ import type { Commit, CycleTimeMetrics, GitHubUsername, PullRequest, RepoFullName, ReviewEngagement, Timestamp } from "../types.cjs";
2
+ import type { CacheFreshness } from "../cache.cjs";
3
+ export type ReportAudience = 'personal' | 'individual' | 'team' | 'exec';
4
+ export interface ReportMeta {
5
+ audience: ReportAudience;
6
+ period_label: string;
7
+ start: Timestamp;
8
+ end: Timestamp;
9
+ generated_at: Timestamp;
10
+ repos: RepoFullName[];
11
+ data_freshness: CacheFreshness;
12
+ }
13
+ export interface PullRequestSummary {
14
+ repo: RepoFullName;
15
+ number: number;
16
+ title: string;
17
+ author: GitHubUsername;
18
+ state: string;
19
+ draft: boolean;
20
+ updated_at: Timestamp;
21
+ created_at: Timestamp;
22
+ age_days: number;
23
+ url?: string;
24
+ }
25
+ export interface CommitSummary {
26
+ repo: RepoFullName;
27
+ sha: string;
28
+ author: GitHubUsername;
29
+ committed_at: Timestamp;
30
+ message: string;
31
+ additions: number;
32
+ deletions: number;
33
+ files_changed: number;
34
+ }
35
+ export interface ReviewDebtSection {
36
+ requested: PullRequestSummary[];
37
+ team_prs: PullRequestSummary[];
38
+ }
39
+ export interface OpenWorkSection {
40
+ open_prs: PullRequestSummary[];
41
+ }
42
+ export interface PersonalSummarySection {
43
+ prs_opened: number;
44
+ prs_merged: number;
45
+ prs_closed: number;
46
+ commits: number;
47
+ repos_touched: number;
48
+ lines_changed: number;
49
+ }
50
+ export interface PersonalReport {
51
+ type: 'personal' | 'individual';
52
+ user: GitHubUsername;
53
+ meta: ReportMeta;
54
+ summary: PersonalSummarySection;
55
+ review_debt: ReviewDebtSection;
56
+ open_work: OpenWorkSection;
57
+ authored_prs: PullRequestSummary[];
58
+ commits_detail: CommitSummary[];
59
+ }
60
+ export interface TeamVelocitySection {
61
+ prs_opened: number;
62
+ prs_merged: number;
63
+ prs_closed: number;
64
+ cycle_time_p50: number | null;
65
+ cycle_time_p90: number | null;
66
+ review_turnaround_p50: number | null;
67
+ review_turnaround_p90: number | null;
68
+ }
69
+ export interface TeamMemberBreakdown {
70
+ user: GitHubUsername;
71
+ prs_authored: number;
72
+ prs_reviewed: number;
73
+ commits: number;
74
+ lines_changed: number;
75
+ }
76
+ export interface BlockerEntry {
77
+ pr: PullRequestSummary;
78
+ status: 'stale' | 'stuck';
79
+ days: number;
80
+ }
81
+ export interface TeamReport {
82
+ type: 'team';
83
+ meta: ReportMeta;
84
+ team_size: number;
85
+ velocity: TeamVelocitySection;
86
+ members: TeamMemberBreakdown[];
87
+ blockers: BlockerEntry[];
88
+ cycle_times: Array<{
89
+ pr: PullRequestSummary;
90
+ metrics: CycleTimeMetrics | null;
91
+ }>;
92
+ review_engagement: Array<{
93
+ pr: PullRequestSummary;
94
+ metrics: ReviewEngagement | null;
95
+ }>;
96
+ }
97
+ export interface TrendPoint {
98
+ bucket_start: Timestamp;
99
+ bucket_end: Timestamp;
100
+ prs_merged: number;
101
+ cycle_time_p50: number | null;
102
+ }
103
+ export interface RepoHealthEntry {
104
+ repo: RepoFullName;
105
+ status: 'active' | 'stale' | 'dormant';
106
+ last_activity_at: Timestamp | null;
107
+ }
108
+ export interface HighlightEntry {
109
+ pr: PullRequestSummary;
110
+ reasons: string[];
111
+ }
112
+ export interface ExecReport {
113
+ type: 'exec';
114
+ meta: ReportMeta;
115
+ velocity_trends: TrendPoint[];
116
+ cycle_time_trends: TrendPoint[];
117
+ repo_health: RepoHealthEntry[];
118
+ highlights: HighlightEntry[];
119
+ team_breakdown: TeamMemberBreakdown[];
120
+ }
121
+ export type Report = PersonalReport | TeamReport | ExecReport;
122
+ export interface ReportInputs {
123
+ repos: RepoFullName[];
124
+ timeRange: {
125
+ start: Timestamp;
126
+ end: Timestamp;
127
+ label: string;
128
+ };
129
+ includeDrafts: boolean;
130
+ blockersOnly: boolean;
131
+ excludeAuthors: Set<GitHubUsername>;
132
+ excludeBots: boolean;
133
+ teamMembers: GitHubUsername[];
134
+ subjectUser?: GitHubUsername;
135
+ thresholds: {
136
+ stale_days: number;
137
+ stuck_days: number;
138
+ large_pr_lines: number;
139
+ };
140
+ }
141
+ export interface ReportDataContext {
142
+ pullRequests: PullRequest[];
143
+ commits: Commit[];
144
+ }
@@ -0,0 +1,144 @@
1
+ import type { Commit, CycleTimeMetrics, GitHubUsername, PullRequest, RepoFullName, ReviewEngagement, Timestamp } from "../types.js";
2
+ import type { CacheFreshness } from "../cache.js";
3
+ export type ReportAudience = 'personal' | 'individual' | 'team' | 'exec';
4
+ export interface ReportMeta {
5
+ audience: ReportAudience;
6
+ period_label: string;
7
+ start: Timestamp;
8
+ end: Timestamp;
9
+ generated_at: Timestamp;
10
+ repos: RepoFullName[];
11
+ data_freshness: CacheFreshness;
12
+ }
13
+ export interface PullRequestSummary {
14
+ repo: RepoFullName;
15
+ number: number;
16
+ title: string;
17
+ author: GitHubUsername;
18
+ state: string;
19
+ draft: boolean;
20
+ updated_at: Timestamp;
21
+ created_at: Timestamp;
22
+ age_days: number;
23
+ url?: string;
24
+ }
25
+ export interface CommitSummary {
26
+ repo: RepoFullName;
27
+ sha: string;
28
+ author: GitHubUsername;
29
+ committed_at: Timestamp;
30
+ message: string;
31
+ additions: number;
32
+ deletions: number;
33
+ files_changed: number;
34
+ }
35
+ export interface ReviewDebtSection {
36
+ requested: PullRequestSummary[];
37
+ team_prs: PullRequestSummary[];
38
+ }
39
+ export interface OpenWorkSection {
40
+ open_prs: PullRequestSummary[];
41
+ }
42
+ export interface PersonalSummarySection {
43
+ prs_opened: number;
44
+ prs_merged: number;
45
+ prs_closed: number;
46
+ commits: number;
47
+ repos_touched: number;
48
+ lines_changed: number;
49
+ }
50
+ export interface PersonalReport {
51
+ type: 'personal' | 'individual';
52
+ user: GitHubUsername;
53
+ meta: ReportMeta;
54
+ summary: PersonalSummarySection;
55
+ review_debt: ReviewDebtSection;
56
+ open_work: OpenWorkSection;
57
+ authored_prs: PullRequestSummary[];
58
+ commits_detail: CommitSummary[];
59
+ }
60
+ export interface TeamVelocitySection {
61
+ prs_opened: number;
62
+ prs_merged: number;
63
+ prs_closed: number;
64
+ cycle_time_p50: number | null;
65
+ cycle_time_p90: number | null;
66
+ review_turnaround_p50: number | null;
67
+ review_turnaround_p90: number | null;
68
+ }
69
+ export interface TeamMemberBreakdown {
70
+ user: GitHubUsername;
71
+ prs_authored: number;
72
+ prs_reviewed: number;
73
+ commits: number;
74
+ lines_changed: number;
75
+ }
76
+ export interface BlockerEntry {
77
+ pr: PullRequestSummary;
78
+ status: 'stale' | 'stuck';
79
+ days: number;
80
+ }
81
+ export interface TeamReport {
82
+ type: 'team';
83
+ meta: ReportMeta;
84
+ team_size: number;
85
+ velocity: TeamVelocitySection;
86
+ members: TeamMemberBreakdown[];
87
+ blockers: BlockerEntry[];
88
+ cycle_times: Array<{
89
+ pr: PullRequestSummary;
90
+ metrics: CycleTimeMetrics | null;
91
+ }>;
92
+ review_engagement: Array<{
93
+ pr: PullRequestSummary;
94
+ metrics: ReviewEngagement | null;
95
+ }>;
96
+ }
97
+ export interface TrendPoint {
98
+ bucket_start: Timestamp;
99
+ bucket_end: Timestamp;
100
+ prs_merged: number;
101
+ cycle_time_p50: number | null;
102
+ }
103
+ export interface RepoHealthEntry {
104
+ repo: RepoFullName;
105
+ status: 'active' | 'stale' | 'dormant';
106
+ last_activity_at: Timestamp | null;
107
+ }
108
+ export interface HighlightEntry {
109
+ pr: PullRequestSummary;
110
+ reasons: string[];
111
+ }
112
+ export interface ExecReport {
113
+ type: 'exec';
114
+ meta: ReportMeta;
115
+ velocity_trends: TrendPoint[];
116
+ cycle_time_trends: TrendPoint[];
117
+ repo_health: RepoHealthEntry[];
118
+ highlights: HighlightEntry[];
119
+ team_breakdown: TeamMemberBreakdown[];
120
+ }
121
+ export type Report = PersonalReport | TeamReport | ExecReport;
122
+ export interface ReportInputs {
123
+ repos: RepoFullName[];
124
+ timeRange: {
125
+ start: Timestamp;
126
+ end: Timestamp;
127
+ label: string;
128
+ };
129
+ includeDrafts: boolean;
130
+ blockersOnly: boolean;
131
+ excludeAuthors: Set<GitHubUsername>;
132
+ excludeBots: boolean;
133
+ teamMembers: GitHubUsername[];
134
+ subjectUser?: GitHubUsername;
135
+ thresholds: {
136
+ stale_days: number;
137
+ stuck_days: number;
138
+ large_pr_lines: number;
139
+ };
140
+ }
141
+ export interface ReportDataContext {
142
+ pullRequests: PullRequest[];
143
+ commits: Commit[];
144
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPullRequestSummary = buildPullRequestSummary;
4
+ exports.computeCycleTime = computeCycleTime;
5
+ exports.computeReviewEngagement = computeReviewEngagement;
6
+ exports.percentile = percentile;
7
+ const time_1 = require("../time.cjs");
8
+ function buildPullRequestSummary(pr, now) {
9
+ return {
10
+ repo: pr.repo,
11
+ number: pr.number,
12
+ title: pr.title,
13
+ author: pr.author,
14
+ state: pr.state,
15
+ draft: pr.draft,
16
+ updated_at: pr.updated_at,
17
+ created_at: pr.created_at,
18
+ age_days: (0, time_1.diffInDays)(pr.created_at, now),
19
+ };
20
+ }
21
+ function computeCycleTime(pr, events) {
22
+ if (pr.merged_at === null) {
23
+ return null;
24
+ }
25
+ const openedAt = minTimestamp(events.filter((event) => event.type === 'pr_opened').map((e) => e.opened_at));
26
+ const readyAt = minTimestamp(events.filter((event) => event.type === 'pr_ready').map((e) => e.ready_at));
27
+ const firstReviewAt = minTimestamp(events.filter((event) => event.type === 'review_submitted').map((event) => event.submitted_at));
28
+ const openTime = openedAt ?? pr.created_at;
29
+ const readyTime = readyAt ?? openTime;
30
+ const reviewStart = firstReviewAt ?? null;
31
+ const draftTime = readyAt ? readyTime - openTime : null;
32
+ const reviewTime = reviewStart ? reviewStart - readyTime : 0;
33
+ const mergeTime = reviewStart ? pr.merged_at - reviewStart : 0;
34
+ const totalTime = pr.merged_at - openTime;
35
+ return {
36
+ draft_time: draftTime,
37
+ review_time: reviewTime,
38
+ merge_time: mergeTime,
39
+ total_time: totalTime,
40
+ };
41
+ }
42
+ function computeReviewEngagement(pr, events) {
43
+ const comments = events.filter((event) => event.type === 'comment_added');
44
+ const reviews = events.filter((event) => event.type === 'review_submitted');
45
+ if (comments.length === 0 && reviews.length === 0) {
46
+ return null;
47
+ }
48
+ const firstComment = minTimestamp(comments.map((event) => event.commented_at));
49
+ const firstReview = minTimestamp(reviews.map((event) => event.submitted_at));
50
+ return {
51
+ time_to_first_comment: firstComment ? firstComment - pr.created_at : null,
52
+ time_to_first_review: firstReview ? firstReview - pr.created_at : null,
53
+ review_count: reviews.length,
54
+ comment_count: comments.length,
55
+ };
56
+ }
57
+ function percentile(values, pct) {
58
+ if (values.length === 0) {
59
+ return null;
60
+ }
61
+ const sorted = [...values].toSorted((a, b) => a - b);
62
+ const index = Math.floor((pct / 100) * (sorted.length - 1));
63
+ return sorted[index] ?? null;
64
+ }
65
+ function minTimestamp(values) {
66
+ const filtered = values.filter((value) => typeof value === 'number');
67
+ if (filtered.length === 0) {
68
+ return null;
69
+ }
70
+ return Math.min(...filtered);
71
+ }
@@ -0,0 +1,6 @@
1
+ import type { CycleTimeMetrics, Event, PullRequest, ReviewEngagement, Timestamp } from "../types.cjs";
2
+ import type { PullRequestSummary } from "./types.cjs";
3
+ export declare function buildPullRequestSummary(pr: PullRequest, now: Timestamp): PullRequestSummary;
4
+ export declare function computeCycleTime(pr: PullRequest, events: Event[]): CycleTimeMetrics | null;
5
+ export declare function computeReviewEngagement(pr: PullRequest, events: Event[]): ReviewEngagement | null;
6
+ export declare function percentile(values: number[], pct: number): number | null;
@@ -0,0 +1,6 @@
1
+ import type { CycleTimeMetrics, Event, PullRequest, ReviewEngagement, Timestamp } from "../types.js";
2
+ import type { PullRequestSummary } from "./types.js";
3
+ export declare function buildPullRequestSummary(pr: PullRequest, now: Timestamp): PullRequestSummary;
4
+ export declare function computeCycleTime(pr: PullRequest, events: Event[]): CycleTimeMetrics | null;
5
+ export declare function computeReviewEngagement(pr: PullRequest, events: Event[]): ReviewEngagement | null;
6
+ export declare function percentile(values: number[], pct: number): number | null;