@oss-autopilot/core 0.42.5 → 0.42.6

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.
@@ -10847,6 +10847,9 @@ var init_display_utils = __esm({
10847
10847
  });
10848
10848
 
10849
10849
  // src/core/github-stats.ts
10850
+ function emptyPRCountsResult() {
10851
+ return { repos: /* @__PURE__ */ new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
10852
+ }
10850
10853
  function isCachedPRCounts(v) {
10851
10854
  if (typeof v !== "object" || v === null) return false;
10852
10855
  const obj = v;
@@ -10854,7 +10857,7 @@ function isCachedPRCounts(v) {
10854
10857
  }
10855
10858
  async function fetchUserPRCounts(octokit, githubUsername, query, label, accumulateRepo) {
10856
10859
  if (!githubUsername) {
10857
- return { repos: /* @__PURE__ */ new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
10860
+ return emptyPRCountsResult();
10858
10861
  }
10859
10862
  const cache = getHttpCache();
10860
10863
  const cacheKey = `pr-counts:${label}:${githubUsername}`;
@@ -10875,6 +10878,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
10875
10878
  const dailyActivityCounts = {};
10876
10879
  let page = 1;
10877
10880
  let fetched = 0;
10881
+ let totalCount;
10878
10882
  while (true) {
10879
10883
  const { data } = await octokit.search.issuesAndPullRequests({
10880
10884
  q: `is:pr ${query} author:${githubUsername}`,
@@ -10883,6 +10887,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
10883
10887
  per_page: 100,
10884
10888
  page
10885
10889
  });
10890
+ totalCount = data.total_count;
10886
10891
  for (const item of data.items) {
10887
10892
  const parsed = extractOwnerRepo(item.html_url);
10888
10893
  if (!parsed) {
@@ -10907,11 +10912,17 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
10907
10912
  }
10908
10913
  }
10909
10914
  fetched += data.items.length;
10910
- if (fetched >= data.total_count || fetched >= 1e3 || data.items.length === 0) {
10915
+ if (fetched >= data.total_count || fetched >= 1e3 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
10911
10916
  break;
10912
10917
  }
10913
10918
  page++;
10914
10919
  }
10920
+ if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
10921
+ warn(
10922
+ MODULE6,
10923
+ `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} ${label} PRs. Stats may be incomplete for prolific contributors.`
10924
+ );
10925
+ }
10915
10926
  debug(MODULE6, `Found ${fetched} ${label} PRs across ${repos.size} repos`);
10916
10927
  cache.set(cacheKey, "", {
10917
10928
  reposEntries: Array.from(repos.entries()),
@@ -11020,7 +11031,7 @@ async function fetchRecentlyMergedPRs(octokit, config, days = 7) {
11020
11031
  }
11021
11032
  );
11022
11033
  }
11023
- var MODULE6, PR_COUNTS_CACHE_TTL_MS;
11034
+ var MODULE6, PR_COUNTS_CACHE_TTL_MS, MAX_PAGINATION_PAGES;
11024
11035
  var init_github_stats = __esm({
11025
11036
  "src/core/github-stats.ts"() {
11026
11037
  "use strict";
@@ -11028,7 +11039,8 @@ var init_github_stats = __esm({
11028
11039
  init_logger();
11029
11040
  init_http_cache();
11030
11041
  MODULE6 = "github-stats";
11031
- PR_COUNTS_CACHE_TTL_MS = 60 * 60 * 1e3;
11042
+ PR_COUNTS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11043
+ MAX_PAGINATION_PAGES = 3;
11032
11044
  }
11033
11045
  });
11034
11046
 
@@ -13476,8 +13488,14 @@ async function fetchPRData(prMonitor, token) {
13476
13488
  const issueMonitor = new IssueConversationMonitor(token);
13477
13489
  const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all(
13478
13490
  [
13479
- prMonitor.fetchUserMergedPRCounts(),
13480
- prMonitor.fetchUserClosedPRCounts(),
13491
+ prMonitor.fetchUserMergedPRCounts().catch((err) => {
13492
+ console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
13493
+ return emptyPRCountsResult();
13494
+ }),
13495
+ prMonitor.fetchUserClosedPRCounts().catch((err) => {
13496
+ console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
13497
+ return emptyPRCountsResult();
13498
+ }),
13481
13499
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
13482
13500
  console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
13483
13501
  return [];
@@ -13626,12 +13644,16 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13626
13644
  function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
13627
13645
  const stateManager2 = getStateManager();
13628
13646
  try {
13629
- stateManager2.setMonthlyMergedCounts(monthlyCounts);
13647
+ if (Object.keys(monthlyCounts).length > 0) {
13648
+ stateManager2.setMonthlyMergedCounts(monthlyCounts);
13649
+ }
13630
13650
  } catch (error) {
13631
13651
  console.error("[DAILY] Failed to store monthly merged counts:", errorMessage(error));
13632
13652
  }
13633
13653
  try {
13634
- stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
13654
+ if (Object.keys(monthlyClosedCounts).length > 0) {
13655
+ stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
13656
+ }
13635
13657
  } catch (error) {
13636
13658
  console.error("[DAILY] Failed to store monthly closed counts:", errorMessage(error));
13637
13659
  }
@@ -13646,7 +13668,9 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
13646
13668
  combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
13647
13669
  }
13648
13670
  }
13649
- stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
13671
+ if (Object.keys(combinedOpenedCounts).length > 0) {
13672
+ stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
13673
+ }
13650
13674
  } catch (error) {
13651
13675
  console.error("[DAILY] Failed to compute/store monthly opened counts:", errorMessage(error));
13652
13676
  }
@@ -13816,6 +13840,7 @@ var init_daily = __esm({
13816
13840
  "use strict";
13817
13841
  init_core();
13818
13842
  init_errors();
13843
+ init_github_stats();
13819
13844
  init_json();
13820
13845
  init_core();
13821
13846
  }
@@ -14542,8 +14567,14 @@ async function fetchDashboardData(token) {
14542
14567
  console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
14543
14568
  return [];
14544
14569
  }),
14545
- prMonitor.fetchUserMergedPRCounts(),
14546
- prMonitor.fetchUserClosedPRCounts(),
14570
+ prMonitor.fetchUserMergedPRCounts().catch((err) => {
14571
+ console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
14572
+ return emptyPRCountsResult();
14573
+ }),
14574
+ prMonitor.fetchUserClosedPRCounts().catch((err) => {
14575
+ console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
14576
+ return emptyPRCountsResult();
14577
+ }),
14547
14578
  issueMonitor.fetchCommentedIssues().catch((error) => {
14548
14579
  const msg = errorMessage(error);
14549
14580
  if (msg.includes("No GitHub username configured")) {
@@ -14567,12 +14598,16 @@ async function fetchDashboardData(token) {
14567
14598
  const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
14568
14599
  const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
14569
14600
  try {
14570
- stateManager2.setMonthlyMergedCounts(monthlyCounts);
14601
+ if (Object.keys(monthlyCounts).length > 0) {
14602
+ stateManager2.setMonthlyMergedCounts(monthlyCounts);
14603
+ }
14571
14604
  } catch (error) {
14572
14605
  console.error("[DASHBOARD] Failed to store monthly merged counts:", errorMessage(error));
14573
14606
  }
14574
14607
  try {
14575
- stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
14608
+ if (Object.keys(monthlyClosedCounts).length > 0) {
14609
+ stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
14610
+ }
14576
14611
  } catch (error) {
14577
14612
  console.error("[DASHBOARD] Failed to store monthly closed counts:", errorMessage(error));
14578
14613
  }
@@ -14587,7 +14622,9 @@ async function fetchDashboardData(token) {
14587
14622
  combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
14588
14623
  }
14589
14624
  }
14590
- stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
14625
+ if (Object.keys(combinedOpenedCounts).length > 0) {
14626
+ stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
14627
+ }
14591
14628
  } catch (error) {
14592
14629
  console.error("[DASHBOARD] Failed to store monthly opened counts:", errorMessage(error));
14593
14630
  }
@@ -14634,6 +14671,7 @@ var init_dashboard_data = __esm({
14634
14671
  "use strict";
14635
14672
  init_core();
14636
14673
  init_errors();
14674
+ init_github_stats();
14637
14675
  init_daily();
14638
14676
  }
14639
14677
  });
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
10
10
  import { errorMessage } from '../core/errors.js';
11
+ import { emptyPRCountsResult } from '../core/github-stats.js';
11
12
  import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
12
13
  // Re-export domain functions so existing consumers (tests, dashboard, startup)
13
14
  // can continue importing from './daily.js' without changes.
@@ -28,11 +29,17 @@ async function fetchPRData(prMonitor, token) {
28
29
  console.error(`Warning: ${failures.length} PR fetch(es) failed`);
29
30
  }
30
31
  // Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
31
- // Recently closed/merged are non-critical (cosmetic sections), so isolate their failure
32
+ // All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
32
33
  const issueMonitor = new IssueConversationMonitor(token);
33
34
  const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
34
- prMonitor.fetchUserMergedPRCounts(),
35
- prMonitor.fetchUserClosedPRCounts(),
35
+ prMonitor.fetchUserMergedPRCounts().catch((err) => {
36
+ console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
37
+ return emptyPRCountsResult();
38
+ }),
39
+ prMonitor.fetchUserClosedPRCounts().catch((err) => {
40
+ console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
41
+ return emptyPRCountsResult();
42
+ }),
36
43
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
37
44
  console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
38
45
  return [];
@@ -195,15 +202,21 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
195
202
  */
196
203
  function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
197
204
  const stateManager = getStateManager();
198
- // Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state)
205
+ // Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state).
206
+ // Guard: skip overwriting when the data is empty to avoid wiping existing chart data on transient API failures.
207
+ // An empty object means the fetch failed and fell back to emptyPRCountsResult(), so we preserve previous state.
199
208
  try {
200
- stateManager.setMonthlyMergedCounts(monthlyCounts);
209
+ if (Object.keys(monthlyCounts).length > 0) {
210
+ stateManager.setMonthlyMergedCounts(monthlyCounts);
211
+ }
201
212
  }
202
213
  catch (error) {
203
214
  console.error('[DAILY] Failed to store monthly merged counts:', errorMessage(error));
204
215
  }
205
216
  try {
206
- stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
217
+ if (Object.keys(monthlyClosedCounts).length > 0) {
218
+ stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
219
+ }
207
220
  }
208
221
  catch (error) {
209
222
  console.error('[DAILY] Failed to store monthly closed counts:', errorMessage(error));
@@ -221,7 +234,9 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
221
234
  combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
222
235
  }
223
236
  }
224
- stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
237
+ if (Object.keys(combinedOpenedCounts).length > 0) {
238
+ stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
239
+ }
225
240
  }
226
241
  catch (error) {
227
242
  console.error('[DAILY] Failed to compute/store monthly opened counts:', errorMessage(error));
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
7
7
  import { errorMessage } from '../core/errors.js';
8
+ import { emptyPRCountsResult } from '../core/github-stats.js';
8
9
  import { toShelvedPRRef } from './daily.js';
9
10
  /**
10
11
  * Fetch fresh dashboard data from GitHub.
@@ -25,8 +26,14 @@ export async function fetchDashboardData(token) {
25
26
  console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
26
27
  return [];
27
28
  }),
28
- prMonitor.fetchUserMergedPRCounts(),
29
- prMonitor.fetchUserClosedPRCounts(),
29
+ prMonitor.fetchUserMergedPRCounts().catch((err) => {
30
+ console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
31
+ return emptyPRCountsResult();
32
+ }),
33
+ prMonitor.fetchUserClosedPRCounts().catch((err) => {
34
+ console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
35
+ return emptyPRCountsResult();
36
+ }),
30
37
  issueMonitor.fetchCommentedIssues().catch((error) => {
31
38
  const msg = errorMessage(error);
32
39
  if (msg.includes('No GitHub username configured')) {
@@ -51,14 +58,19 @@ export async function fetchDashboardData(token) {
51
58
  // Store monthly chart data (opened/merged/closed) so charts have data
52
59
  const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
53
60
  const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
61
+ // Guard: skip overwriting when data is empty to avoid wiping chart data on transient API failures.
54
62
  try {
55
- stateManager.setMonthlyMergedCounts(monthlyCounts);
63
+ if (Object.keys(monthlyCounts).length > 0) {
64
+ stateManager.setMonthlyMergedCounts(monthlyCounts);
65
+ }
56
66
  }
57
67
  catch (error) {
58
68
  console.error('[DASHBOARD] Failed to store monthly merged counts:', errorMessage(error));
59
69
  }
60
70
  try {
61
- stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
71
+ if (Object.keys(monthlyClosedCounts).length > 0) {
72
+ stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
73
+ }
62
74
  }
63
75
  catch (error) {
64
76
  console.error('[DASHBOARD] Failed to store monthly closed counts:', errorMessage(error));
@@ -74,7 +86,9 @@ export async function fetchDashboardData(token) {
74
86
  combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
75
87
  }
76
88
  }
77
- stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
89
+ if (Object.keys(combinedOpenedCounts).length > 0) {
90
+ stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
91
+ }
78
92
  }
79
93
  catch (error) {
80
94
  console.error('[DASHBOARD] Failed to store monthly opened counts:', errorMessage(error));
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
6
  import { ClosedPR, MergedPR } from './types.js';
7
- /** TTL for cached PR count results (1 hour). */
7
+ /** TTL for cached PR count results (24 hours — these stats change slowly). */
8
8
  export declare const PR_COUNTS_CACHE_TTL_MS: number;
9
9
  /** Return type shared by both merged and closed PR count functions. */
10
10
  export interface PRCountsResult<R> {
@@ -13,6 +13,8 @@ export interface PRCountsResult<R> {
13
13
  monthlyOpenedCounts: Record<string, number>;
14
14
  dailyActivityCounts: Record<string, number>;
15
15
  }
16
+ /** Returns an empty PRCountsResult. Used as the fallback when stats fetches fail or username is empty. */
17
+ export declare function emptyPRCountsResult<R>(): PRCountsResult<R>;
16
18
  /**
17
19
  * Fetch merged PR counts and latest merge dates per repository for the configured user.
18
20
  * Also builds a monthly histogram of all merges for the contribution timeline.
@@ -6,8 +6,18 @@ import { extractOwnerRepo, parseGitHubUrl, isOwnRepo } from './utils.js';
6
6
  import { debug, warn } from './logger.js';
7
7
  import { getHttpCache } from './http-cache.js';
8
8
  const MODULE = 'github-stats';
9
- /** TTL for cached PR count results (1 hour). */
10
- export const PR_COUNTS_CACHE_TTL_MS = 60 * 60 * 1000;
9
+ /** TTL for cached PR count results (24 hours — these stats change slowly). */
10
+ export const PR_COUNTS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
11
+ /**
12
+ * Maximum number of pages to fetch during paginated PR count searches.
13
+ * GitHub's Search API becomes unreliable (500s, ECONNRESET) at deep pages (4-5+).
14
+ * 3 pages × 100 per_page = 300 results, which covers the vast majority of users.
15
+ */
16
+ const MAX_PAGINATION_PAGES = 3;
17
+ /** Returns an empty PRCountsResult. Used as the fallback when stats fetches fail or username is empty. */
18
+ export function emptyPRCountsResult() {
19
+ return { repos: new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
20
+ }
11
21
  /** Type guard for deserialized cache data — prevents crashes on corrupt/stale cache. */
12
22
  function isCachedPRCounts(v) {
13
23
  if (typeof v !== 'object' || v === null)
@@ -31,7 +41,7 @@ function isCachedPRCounts(v) {
31
41
  */
32
42
  async function fetchUserPRCounts(octokit, githubUsername, query, label, accumulateRepo) {
33
43
  if (!githubUsername) {
34
- return { repos: new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
44
+ return emptyPRCountsResult();
35
45
  }
36
46
  // Check for a fresh cached result (avoids 10-20 paginated API calls)
37
47
  const cache = getHttpCache();
@@ -53,6 +63,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
53
63
  const dailyActivityCounts = {};
54
64
  let page = 1;
55
65
  let fetched = 0;
66
+ let totalCount;
56
67
  while (true) {
57
68
  const { data } = await octokit.search.issuesAndPullRequests({
58
69
  q: `is:pr ${query} author:${githubUsername}`,
@@ -61,6 +72,7 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
61
72
  per_page: 100,
62
73
  page,
63
74
  });
75
+ totalCount = data.total_count;
64
76
  for (const item of data.items) {
65
77
  const parsed = extractOwnerRepo(item.html_url);
66
78
  if (!parsed) {
@@ -95,14 +107,22 @@ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumula
95
107
  }
96
108
  }
97
109
  fetched += data.items.length;
98
- // Stop if we've fetched all results or hit the API limit (1000)
99
- if (fetched >= data.total_count || fetched >= 1000 || data.items.length === 0) {
110
+ // Stop if we've fetched all results, hit the API limit (1000), or reached max pages
111
+ // GitHub Search API returns 500/ECONNRESET on deep pagination (page 4-5+),
112
+ // so we cap at MAX_PAGINATION_PAGES to avoid taking down the entire startup flow.
113
+ if (fetched >= data.total_count || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
100
114
  break;
101
115
  }
102
116
  page++;
103
117
  }
118
+ if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
119
+ warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} ${label} PRs. Stats may be incomplete for prolific contributors.`);
120
+ }
104
121
  debug(MODULE, `Found ${fetched} ${label} PRs across ${repos.size} repos`);
105
- // Cache the aggregated result (Map → entries array for JSON serialization)
122
+ // Cache the aggregated result (Map → entries array for JSON serialization).
123
+ // Note: truncated results (from pagination cap) are cached with the same TTL. This is an
124
+ // accepted tradeoff — prolific contributors (300+ PRs) may see slightly incomplete stats
125
+ // for up to 24 hours, but these are cosmetic and self-heal on the next cache expiry.
106
126
  cache.set(cacheKey, '', {
107
127
  reposEntries: Array.from(repos.entries()),
108
128
  monthlyCounts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.42.5",
3
+ "version": "0.42.6",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {