@oss-autopilot/core 0.44.2 → 0.44.3

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.
@@ -8,8 +8,8 @@ export { IssueDiscovery, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } fro
8
8
  export { IssueConversationMonitor } from './issue-conversation.js';
9
9
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  export { getOctokit, checkRateLimit } from './github.js';
11
- export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
12
- export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
11
+ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, DEFAULT_CONCURRENCY, } from './utils.js';
12
+ export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
13
13
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
14
14
  export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
15
15
  export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
@@ -9,12 +9,12 @@ import { getOctokit } from './github.js';
9
9
  import { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  import { paginateAll } from './pagination.js';
11
11
  import { getStateManager } from './state.js';
12
- import { daysBetween, splitRepo, extractOwnerRepo, isOwnRepo } from './utils.js';
12
+ import { daysBetween, splitRepo, extractOwnerRepo, isOwnRepo, DEFAULT_CONCURRENCY } from './utils.js';
13
13
  import { runWorkerPool } from './concurrency.js';
14
14
  import { ConfigurationError, errorMessage } from './errors.js';
15
15
  import { debug, warn } from './logger.js';
16
16
  const MODULE = 'issue-conversation';
17
- const MAX_CONCURRENT_REQUESTS = 5;
17
+ const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
18
18
  /** Associations that indicate someone with repo-level permissions. */
19
19
  const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
20
20
  export class IssueConversationMonitor {
@@ -65,11 +65,6 @@ export declare class IssueDiscovery {
65
65
  * Split repos into batches of the specified size.
66
66
  */
67
67
  private batchRepos;
68
- /**
69
- * Check if an error is a GitHub rate limit error (429 or rate-limit 403).
70
- * Static proxy kept for backward compatibility with tests.
71
- */
72
- static isRateLimitError(error: unknown): boolean;
73
68
  /**
74
69
  * Vet a specific issue (delegates to IssueVetter).
75
70
  */
@@ -12,7 +12,7 @@ import { getOctokit, checkRateLimit } from './github.js';
12
12
  import { getStateManager } from './state.js';
13
13
  import { daysBetween, getDataDir } from './utils.js';
14
14
  import { DEFAULT_CONFIG } from './types.js';
15
- import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
15
+ import { ValidationError, errorMessage, getHttpStatusCode, isRateLimitError } from './errors.js';
16
16
  import { debug, info, warn } from './logger.js';
17
17
  import { getHttpCache, cachedTimeBased } from './http-cache.js';
18
18
  import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
@@ -328,7 +328,7 @@ export class IssueDiscovery {
328
328
  catch (error) {
329
329
  const errMsg = errorMessage(error);
330
330
  phase2Error = errMsg;
331
- if (IssueVetter.isRateLimitError(error)) {
331
+ if (isRateLimitError(error)) {
332
332
  rateLimitHitDuringSearch = true;
333
333
  }
334
334
  warn(MODULE, `Error in general issue search: ${errMsg}`);
@@ -370,7 +370,7 @@ export class IssueDiscovery {
370
370
  catch (error) {
371
371
  const errMsg = errorMessage(error);
372
372
  phase3Error = errMsg;
373
- if (IssueVetter.isRateLimitError(error)) {
373
+ if (isRateLimitError(error)) {
374
374
  rateLimitHitDuringSearch = true;
375
375
  }
376
376
  warn(MODULE, `Error in maintained-repo search: ${errMsg}`);
@@ -464,7 +464,7 @@ export class IssueDiscovery {
464
464
  }
465
465
  catch (error) {
466
466
  failedBatches++;
467
- if (IssueVetter.isRateLimitError(error)) {
467
+ if (isRateLimitError(error)) {
468
468
  rateLimitFailures++;
469
469
  }
470
470
  const batchRepos = batch.join(', ');
@@ -489,13 +489,6 @@ export class IssueDiscovery {
489
489
  }
490
490
  return batches;
491
491
  }
492
- /**
493
- * Check if an error is a GitHub rate limit error (429 or rate-limit 403).
494
- * Static proxy kept for backward compatibility with tests.
495
- */
496
- static isRateLimitError(error) {
497
- return IssueVetter.isRateLimitError(error);
498
- }
499
492
  /**
500
493
  * Vet a specific issue (delegates to IssueVetter).
501
494
  */
@@ -29,8 +29,6 @@ export declare class IssueVetter {
29
29
  allFailed: boolean;
30
30
  rateLimitHit: boolean;
31
31
  }>;
32
- /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
33
- static isRateLimitError(error: unknown): boolean;
34
32
  checkNoExistingPR(owner: string, repo: string, issueNumber: number): Promise<CheckResult>;
35
33
  /**
36
34
  * Check how many merged PRs the authenticated user has in a repo.
@@ -5,14 +5,13 @@
5
5
  * Extracted from issue-discovery.ts (#356) to isolate vetting logic.
6
6
  */
7
7
  import { paginateAll } from './pagination.js';
8
- import { parseGitHubUrl, daysBetween } from './utils.js';
9
- import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
8
+ import { parseGitHubUrl, daysBetween, DEFAULT_CONCURRENCY } from './utils.js';
9
+ import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
10
10
  import { warn } from './logger.js';
11
11
  import { getHttpCache, cachedRequest, cachedTimeBased } from './http-cache.js';
12
12
  import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
13
13
  const MODULE = 'issue-vetting';
14
- // Concurrency limit for parallel API calls
15
- const MAX_CONCURRENT_REQUESTS = 5;
14
+ const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
16
15
  // Cache for contribution guidelines (expires after 1 hour, max 100 entries)
17
16
  const guidelinesCache = new Map();
18
17
  const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -233,7 +232,7 @@ export class IssueVetter {
233
232
  */
234
233
  async vetIssuesParallel(urls, maxResults, priority) {
235
234
  const candidates = [];
236
- const pending = [];
235
+ const pending = new Map();
237
236
  let failedVettingCount = 0;
238
237
  let rateLimitFailures = 0;
239
238
  let attemptedCount = 0;
@@ -253,21 +252,20 @@ export class IssueVetter {
253
252
  })
254
253
  .catch((error) => {
255
254
  failedVettingCount++;
256
- if (IssueVetter.isRateLimitError(error)) {
255
+ if (isRateLimitError(error)) {
257
256
  rateLimitFailures++;
258
257
  }
259
258
  warn(MODULE, `Error vetting issue ${url}:`, errorMessage(error));
260
- });
261
- pending.push(task);
262
- // Limit concurrency
263
- if (pending.length >= MAX_CONCURRENT_REQUESTS) {
264
- // Wait for at least one to complete, then remove it
265
- const completed = await Promise.race(pending.map((p, i) => p.then(() => i)));
266
- pending.splice(completed, 1);
259
+ })
260
+ .finally(() => pending.delete(url));
261
+ pending.set(url, task);
262
+ // Limit concurrency wait for at least one to complete before launching more
263
+ if (pending.size >= MAX_CONCURRENT_REQUESTS) {
264
+ await Promise.race(pending.values());
267
265
  }
268
266
  }
269
267
  // Wait for remaining
270
- await Promise.allSettled(pending);
268
+ await Promise.allSettled(pending.values());
271
269
  const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
272
270
  if (allFailed) {
273
271
  warn(MODULE, `All ${attemptedCount} issue(s) failed vetting. ` +
@@ -275,17 +273,6 @@ export class IssueVetter {
275
273
  }
276
274
  return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
277
275
  }
278
- /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
279
- static isRateLimitError(error) {
280
- const status = getHttpStatusCode(error);
281
- if (status === 429)
282
- return true;
283
- if (status === 403) {
284
- const msg = errorMessage(error).toLowerCase();
285
- return msg.includes('rate limit');
286
- }
287
- return false;
288
- }
289
276
  async checkNoExistingPR(owner, repo, issueNumber) {
290
277
  try {
291
278
  // Search for PRs that mention this issue
@@ -447,26 +434,25 @@ export class IssueVetter {
447
434
  return cached.guidelines;
448
435
  }
449
436
  const filesToCheck = ['CONTRIBUTING.md', '.github/CONTRIBUTING.md', 'docs/CONTRIBUTING.md', 'contributing.md'];
450
- for (const file of filesToCheck) {
451
- try {
452
- const { data } = await this.octokit.repos.getContent({
453
- owner,
454
- repo,
455
- path: file,
456
- });
457
- if ('content' in data) {
458
- const content = Buffer.from(data.content, 'base64').toString('utf-8');
459
- const guidelines = this.parseContributionGuidelines(content);
460
- // Cache the result and prune if needed
461
- guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
462
- pruneCache();
463
- return guidelines;
464
- }
437
+ // Probe all paths in parallel — take the first success in priority order
438
+ const results = await Promise.allSettled(filesToCheck.map((file) => this.octokit.repos.getContent({ owner, repo, path: file }).then(({ data }) => {
439
+ if ('content' in data) {
440
+ return Buffer.from(data.content, 'base64').toString('utf-8');
441
+ }
442
+ return null;
443
+ })));
444
+ for (let i = 0; i < results.length; i++) {
445
+ const result = results[i];
446
+ if (result.status === 'fulfilled' && result.value) {
447
+ const guidelines = this.parseContributionGuidelines(result.value);
448
+ guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
449
+ pruneCache();
450
+ return guidelines;
465
451
  }
466
- catch (error) {
467
- // File not found is expected; only log unexpected errors
468
- if (error instanceof Error && !error.message.includes('404') && !error.message.includes('Not Found')) {
469
- warn(MODULE, `Unexpected error fetching ${file} from ${owner}/${repo}: ${error.message}`);
452
+ if (result.status === 'rejected') {
453
+ const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
454
+ if (!msg.includes('404') && !msg.includes('Not Found')) {
455
+ warn(MODULE, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${msg}`);
470
456
  }
471
457
  }
472
458
  }
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { getOctokit } from './github.js';
15
15
  import { getStateManager } from './state.js';
16
- import { daysBetween, parseGitHubUrl, extractOwnerRepo } from './utils.js';
16
+ import { daysBetween, parseGitHubUrl, extractOwnerRepo, DEFAULT_CONCURRENCY } from './utils.js';
17
17
  import { runWorkerPool } from './concurrency.js';
18
18
  import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
19
19
  import { paginateAll } from './pagination.js';
@@ -31,8 +31,7 @@ export { computeDisplayLabel } from './display-utils.js';
31
31
  export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
32
32
  export { isConditionalChecklistItem } from './checklist-analysis.js';
33
33
  const MODULE = 'pr-monitor';
34
- // Concurrency limit for parallel API calls
35
- const MAX_CONCURRENT_REQUESTS = 5;
34
+ const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
36
35
  export class PRMonitor {
37
36
  octokit;
38
37
  stateManager;
@@ -482,7 +481,7 @@ export class PRMonitor {
482
481
  }
483
482
  // If entire chunk failed, likely a systemic issue (rate limit, auth, outage) — abort remaining
484
483
  if (chunkFailures === chunk.length && chunk.length > 0) {
485
- const remaining = repos.length - i - chunkSize;
484
+ const remaining = uniqueRepos.length - i - chunkSize;
486
485
  if (remaining > 0) {
487
486
  warn(MODULE, `Entire chunk failed, aborting remaining ${remaining} repos`);
488
487
  }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Shared utility functions
3
3
  */
4
+ /** Default concurrency limit for parallel GitHub API requests. */
5
+ export declare const DEFAULT_CONCURRENCY = 5;
4
6
  /**
5
7
  * Returns the oss-autopilot data directory path, creating it if it does not exist.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Shared utility functions
3
3
  */
4
+ /** Default concurrency limit for parallel GitHub API requests. */
5
+ export const DEFAULT_CONCURRENCY = 5;
4
6
  import * as fs from 'fs';
5
7
  import * as path from 'path';
6
8
  import * as os from 'os';
@@ -207,7 +209,7 @@ export function extractOwnerRepo(url) {
207
209
  * // -9
208
210
  */
209
211
  export function daysBetween(from, to = new Date()) {
210
- return Math.floor((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24));
212
+ return Math.max(0, Math.floor((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24)));
211
213
  }
212
214
  /**
213
215
  * Splits an `"owner/repo"` string into its owner and repo components.
@@ -268,6 +270,8 @@ export function getCLIVersion() {
268
270
  export function formatRelativeTime(dateStr) {
269
271
  const date = new Date(dateStr);
270
272
  const diffMs = Date.now() - date.getTime();
273
+ if (diffMs < 0)
274
+ return 'just now';
271
275
  const diffMins = Math.floor(diffMs / 60000);
272
276
  const diffHours = Math.floor(diffMs / 3600000);
273
277
  const diffDays = Math.floor(diffMs / 86400000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.44.2",
3
+ "version": "0.44.3",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {