@oss-autopilot/core 0.42.0 → 0.42.2
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.
- package/dist/cli.bundle.cjs +1026 -1018
- package/dist/cli.js +18 -30
- package/dist/commands/check-integration.js +5 -4
- package/dist/commands/comments.js +24 -24
- package/dist/commands/daily.d.ts +0 -1
- package/dist/commands/daily.js +18 -16
- package/dist/commands/dashboard-components.d.ts +33 -0
- package/dist/commands/dashboard-components.js +57 -0
- package/dist/commands/dashboard-data.js +7 -6
- package/dist/commands/dashboard-formatters.d.ts +20 -0
- package/dist/commands/dashboard-formatters.js +33 -0
- package/dist/commands/dashboard-scripts.d.ts +7 -0
- package/dist/commands/dashboard-scripts.js +281 -0
- package/dist/commands/dashboard-server.js +3 -2
- package/dist/commands/dashboard-styles.d.ts +5 -0
- package/dist/commands/dashboard-styles.js +765 -0
- package/dist/commands/dashboard-templates.d.ts +6 -18
- package/dist/commands/dashboard-templates.js +30 -1134
- package/dist/commands/dashboard.js +2 -1
- package/dist/commands/dismiss.d.ts +6 -6
- package/dist/commands/dismiss.js +13 -13
- package/dist/commands/local-repos.js +2 -1
- package/dist/commands/parse-list.js +2 -1
- package/dist/commands/startup.js +6 -16
- package/dist/commands/validation.d.ts +3 -1
- package/dist/commands/validation.js +12 -6
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/github-stats.d.ts +14 -21
- package/dist/core/github-stats.js +84 -138
- package/dist/core/http-cache.d.ts +6 -0
- package/dist/core/http-cache.js +16 -4
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +3 -2
- package/dist/core/issue-conversation.js +4 -4
- package/dist/core/issue-discovery.d.ts +5 -0
- package/dist/core/issue-discovery.js +70 -93
- package/dist/core/issue-vetting.js +17 -17
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/pr-monitor.d.ts +6 -20
- package/dist/core/pr-monitor.js +16 -52
- package/dist/core/review-analysis.js +8 -6
- package/dist/core/state.js +4 -5
- package/dist/core/test-utils.d.ts +14 -0
- package/dist/core/test-utils.js +125 -0
- package/dist/core/utils.d.ts +11 -0
- package/dist/core/utils.js +21 -0
- package/dist/formatters/json.d.ts +0 -1
- package/package.json +1 -1
package/dist/core/http-cache.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from 'path';
|
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
15
|
import { getCacheDir } from './utils.js';
|
|
16
16
|
import { debug } from './logger.js';
|
|
17
|
+
import { getHttpStatusCode } from './errors.js';
|
|
17
18
|
const MODULE = 'http-cache';
|
|
18
19
|
/**
|
|
19
20
|
* Maximum age (in ms) before a cache entry is considered stale and eligible for
|
|
@@ -44,6 +45,20 @@ export class HttpCache {
|
|
|
44
45
|
pathFor(url) {
|
|
45
46
|
return path.join(this.cacheDir, `${this.keyFor(url)}.json`);
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Return the cached body if the entry exists and is younger than `maxAgeMs`.
|
|
50
|
+
* Useful for time-based caching where ETag validation isn't applicable
|
|
51
|
+
* (e.g., caching aggregated results from paginated API calls).
|
|
52
|
+
*/
|
|
53
|
+
getIfFresh(key, maxAgeMs) {
|
|
54
|
+
const entry = this.get(key);
|
|
55
|
+
if (!entry)
|
|
56
|
+
return null;
|
|
57
|
+
const age = Date.now() - new Date(entry.cachedAt).getTime();
|
|
58
|
+
if (!Number.isFinite(age) || age < 0 || age > maxAgeMs)
|
|
59
|
+
return null;
|
|
60
|
+
return entry.body;
|
|
61
|
+
}
|
|
47
62
|
/**
|
|
48
63
|
* Look up a cached response. Returns `null` if no cache entry exists.
|
|
49
64
|
*/
|
|
@@ -262,8 +277,5 @@ export async function cachedRequest(cache, url, fetcher) {
|
|
|
262
277
|
* Octokit throws a RequestError with status 304 for conditional requests.
|
|
263
278
|
*/
|
|
264
279
|
function isNotModifiedError(err) {
|
|
265
|
-
|
|
266
|
-
return err.status === 304;
|
|
267
|
-
}
|
|
268
|
-
return false;
|
|
280
|
+
return getHttpStatusCode(err) === 304;
|
|
269
281
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,8 +8,9 @@ export { IssueDiscovery, type IssueCandidate, type SearchPriority, isDocOnlyIssu
|
|
|
8
8
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
9
9
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
10
10
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
11
|
-
export { parseGitHubUrl, daysBetween, splitRepo, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
|
|
12
|
-
export {
|
|
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';
|
|
13
|
+
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
13
14
|
export { HttpCache, getHttpCache, resetHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
14
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
15
16
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -8,8 +8,9 @@ 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, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
|
|
12
|
-
export {
|
|
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';
|
|
13
|
+
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
13
14
|
export { HttpCache, getHttpCache, resetHttpCache, cachedRequest } from './http-cache.js';
|
|
14
15
|
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
15
16
|
export * from './types.js';
|
|
@@ -9,9 +9,9 @@ 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 } from './utils.js';
|
|
12
|
+
import { daysBetween, splitRepo, extractOwnerRepo, isOwnRepo } from './utils.js';
|
|
13
13
|
import { runWorkerPool } from './concurrency.js';
|
|
14
|
-
import { ConfigurationError } from './errors.js';
|
|
14
|
+
import { ConfigurationError, errorMessage } from './errors.js';
|
|
15
15
|
import { debug, warn } from './logger.js';
|
|
16
16
|
const MODULE = 'issue-conversation';
|
|
17
17
|
const MAX_CONCURRENT_REQUESTS = 5;
|
|
@@ -71,7 +71,7 @@ export class IssueConversationMonitor {
|
|
|
71
71
|
const { owner, repo } = parsed;
|
|
72
72
|
const repoFullName = `${owner}/${repo}`;
|
|
73
73
|
// Skip issues in user-owned repos (we only care about contributing to others' projects)
|
|
74
|
-
if (owner
|
|
74
|
+
if (isOwnRepo(owner, username))
|
|
75
75
|
continue;
|
|
76
76
|
// Skip user-authored issues
|
|
77
77
|
if (item.user?.login?.toLowerCase() === username.toLowerCase())
|
|
@@ -107,7 +107,7 @@ export class IssueConversationMonitor {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
catch (error) {
|
|
110
|
-
const msg =
|
|
110
|
+
const msg = errorMessage(error);
|
|
111
111
|
failures.push({ issueUrl: item.html_url, error: msg });
|
|
112
112
|
warn(MODULE, `Error analyzing issue ${item.html_url}: ${msg}`);
|
|
113
113
|
}
|
|
@@ -29,6 +29,11 @@ export declare class IssueDiscovery {
|
|
|
29
29
|
* Get starred repos, fetching from GitHub if cache is stale
|
|
30
30
|
*/
|
|
31
31
|
getStarredReposWithRefresh(): Promise<string[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
34
|
+
* Extracts the common logic so each phase only needs to supply search results and context.
|
|
35
|
+
*/
|
|
36
|
+
private filterVetAndScore;
|
|
32
37
|
/**
|
|
33
38
|
* Search for issues matching our criteria.
|
|
34
39
|
* Searches in priority order: merged-PR repos first (no label filter), then starred repos,
|
|
@@ -12,8 +12,8 @@ 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 } from './errors.js';
|
|
16
|
-
import { warn } from './logger.js';
|
|
15
|
+
import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
16
|
+
import { debug, info, warn } from './logger.js';
|
|
17
17
|
import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
|
|
18
18
|
import { IssueVetter } from './issue-vetting.js';
|
|
19
19
|
import { calculateViabilityScore as calcViabilityScore } from './issue-scoring.js';
|
|
@@ -40,7 +40,7 @@ export class IssueDiscovery {
|
|
|
40
40
|
* Updates the state manager with the list and timestamp.
|
|
41
41
|
*/
|
|
42
42
|
async fetchStarredRepos() {
|
|
43
|
-
|
|
43
|
+
info(MODULE, 'Fetching starred repositories...');
|
|
44
44
|
const starredRepos = [];
|
|
45
45
|
try {
|
|
46
46
|
// Paginate through all starred repos (up to 500 to avoid excessive API calls)
|
|
@@ -68,26 +68,26 @@ export class IssueDiscovery {
|
|
|
68
68
|
pageCount++;
|
|
69
69
|
// Limit to 5 pages (500 repos) to avoid excessive API usage
|
|
70
70
|
if (pageCount >= 5) {
|
|
71
|
-
|
|
71
|
+
info(MODULE, 'Reached pagination limit for starred repos (500)');
|
|
72
72
|
break;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
info(MODULE, `Fetched ${starredRepos.length} starred repositories`);
|
|
76
76
|
this.stateManager.setStarredRepos(starredRepos);
|
|
77
77
|
return starredRepos;
|
|
78
78
|
}
|
|
79
79
|
catch (error) {
|
|
80
80
|
const cachedRepos = this.stateManager.getStarredRepos();
|
|
81
|
-
const
|
|
82
|
-
warn(MODULE, 'Error fetching starred repos:',
|
|
81
|
+
const errMsg = errorMessage(error);
|
|
82
|
+
warn(MODULE, 'Error fetching starred repos:', errMsg);
|
|
83
83
|
if (cachedRepos.length === 0) {
|
|
84
84
|
warn(MODULE, `Failed to fetch starred repositories from GitHub API. ` +
|
|
85
|
-
`No cached repos available. Error: ${
|
|
85
|
+
`No cached repos available. Error: ${errMsg}\n` +
|
|
86
86
|
`Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`);
|
|
87
87
|
}
|
|
88
88
|
else {
|
|
89
89
|
warn(MODULE, `Failed to fetch starred repositories from GitHub API. ` +
|
|
90
|
-
`Using ${cachedRepos.length} cached repos instead. Error: ${
|
|
90
|
+
`Using ${cachedRepos.length} cached repos instead. Error: ${errMsg}`);
|
|
91
91
|
}
|
|
92
92
|
return cachedRepos;
|
|
93
93
|
}
|
|
@@ -101,6 +101,41 @@ export class IssueDiscovery {
|
|
|
101
101
|
}
|
|
102
102
|
return this.stateManager.getStarredRepos();
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
106
|
+
* Extracts the common logic so each phase only needs to supply search results and context.
|
|
107
|
+
*/
|
|
108
|
+
async filterVetAndScore(items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
|
|
109
|
+
const spamRepos = detectLabelFarmingRepos(items);
|
|
110
|
+
if (spamRepos.size > 0) {
|
|
111
|
+
const spamCount = items.filter((i) => spamRepos.has(i.repository_url.split('/').slice(-2).join('/'))).length;
|
|
112
|
+
debug(MODULE, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
const itemsToVet = filterIssues(items)
|
|
115
|
+
.filter((item) => {
|
|
116
|
+
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
117
|
+
if (spamRepos.has(repoFullName))
|
|
118
|
+
return false;
|
|
119
|
+
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
120
|
+
})
|
|
121
|
+
.slice(0, remainingNeeded * 2);
|
|
122
|
+
if (itemsToVet.length === 0) {
|
|
123
|
+
debug(MODULE, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
124
|
+
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
125
|
+
}
|
|
126
|
+
const { candidates: results, allFailed: allVetFailed, rateLimitHit, } = await this.vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, 'normal');
|
|
127
|
+
const starFiltered = results.filter((c) => {
|
|
128
|
+
if (c.projectHealth.checkFailed)
|
|
129
|
+
return true;
|
|
130
|
+
const stars = c.projectHealth.stargazersCount ?? 0;
|
|
131
|
+
return stars >= minStars;
|
|
132
|
+
});
|
|
133
|
+
const starFilteredCount = results.length - starFiltered.length;
|
|
134
|
+
if (starFilteredCount > 0) {
|
|
135
|
+
debug(MODULE, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
|
|
136
|
+
}
|
|
137
|
+
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
138
|
+
}
|
|
104
139
|
/**
|
|
105
140
|
* Search for issues matching our criteria.
|
|
106
141
|
* Searches in priority order: merged-PR repos first (no label filter), then starred repos,
|
|
@@ -112,6 +147,7 @@ export class IssueDiscovery {
|
|
|
112
147
|
const languages = options.languages || config.languages;
|
|
113
148
|
const labels = options.labels || config.labels;
|
|
114
149
|
const maxResults = options.maxResults || 10;
|
|
150
|
+
const minStars = config.minStars ?? 50;
|
|
115
151
|
const allCandidates = [];
|
|
116
152
|
let phase0Error = null;
|
|
117
153
|
let phase1Error = null;
|
|
@@ -128,11 +164,11 @@ export class IssueDiscovery {
|
|
|
128
164
|
}
|
|
129
165
|
catch (error) {
|
|
130
166
|
// Fail fast on auth errors — no point searching with a bad token
|
|
131
|
-
if (error
|
|
167
|
+
if (getHttpStatusCode(error) === 401) {
|
|
132
168
|
throw error;
|
|
133
169
|
}
|
|
134
170
|
// Non-fatal: proceed with search for transient/network errors
|
|
135
|
-
warn(MODULE, 'Could not check rate limit:', error
|
|
171
|
+
warn(MODULE, 'Could not check rate limit:', errorMessage(error));
|
|
136
172
|
}
|
|
137
173
|
// Get merged-PR repos (highest merge probability)
|
|
138
174
|
const mergedPRRepos = this.stateManager.getReposWithMergedPRs();
|
|
@@ -162,7 +198,7 @@ export class IssueDiscovery {
|
|
|
162
198
|
const includeDocIssues = config.includeDocIssues ?? true;
|
|
163
199
|
const aiBlocklisted = new Set(config.aiPolicyBlocklist ?? DEFAULT_CONFIG.aiPolicyBlocklist ?? []);
|
|
164
200
|
if (aiBlocklisted.size > 0) {
|
|
165
|
-
|
|
201
|
+
debug(MODULE, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(', ')}`);
|
|
166
202
|
}
|
|
167
203
|
const filterIssues = (items) => {
|
|
168
204
|
return items.filter((item) => {
|
|
@@ -196,7 +232,7 @@ export class IssueDiscovery {
|
|
|
196
232
|
if (phase0Repos.length > 0) {
|
|
197
233
|
const mergedInPhase0 = Math.min(mergedPRRepos.length, phase0Repos.length);
|
|
198
234
|
const openInPhase0 = phase0Repos.length - mergedInPhase0;
|
|
199
|
-
|
|
235
|
+
info(MODULE, `Phase 0: Searching issues in ${phase0Repos.length} repos (${mergedInPhase0} merged-PR, ${openInPhase0} open-PR, no label filter)...`);
|
|
200
236
|
// Phase 0a: merged-PR repos (priority: merged_pr)
|
|
201
237
|
const mergedPhase0Repos = phase0Repos.slice(0, mergedInPhase0);
|
|
202
238
|
if (mergedPhase0Repos.length > 0) {
|
|
@@ -210,7 +246,7 @@ export class IssueDiscovery {
|
|
|
210
246
|
if (rateLimitHit) {
|
|
211
247
|
rateLimitHitDuringSearch = true;
|
|
212
248
|
}
|
|
213
|
-
|
|
249
|
+
info(MODULE, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
|
|
214
250
|
}
|
|
215
251
|
}
|
|
216
252
|
// Phase 0b: open-PR repos (priority: starred — intermediate tier)
|
|
@@ -227,7 +263,7 @@ export class IssueDiscovery {
|
|
|
227
263
|
if (rateLimitHit) {
|
|
228
264
|
rateLimitHitDuringSearch = true;
|
|
229
265
|
}
|
|
230
|
-
|
|
266
|
+
info(MODULE, `Found ${openCandidates.length} candidates from open-PR repos`);
|
|
231
267
|
}
|
|
232
268
|
}
|
|
233
269
|
}
|
|
@@ -235,7 +271,7 @@ export class IssueDiscovery {
|
|
|
235
271
|
if (allCandidates.length < maxResults && starredRepos.length > 0) {
|
|
236
272
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
237
273
|
if (reposToSearch.length > 0) {
|
|
238
|
-
|
|
274
|
+
info(MODULE, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
|
|
239
275
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
240
276
|
if (remainingNeeded > 0) {
|
|
241
277
|
const { candidates: starredCandidates, allBatchesFailed, rateLimitHit, } = await this.searchInRepos(reposToSearch.slice(0, 10), baseQuery, remainingNeeded, 'starred', filterIssues);
|
|
@@ -246,14 +282,14 @@ export class IssueDiscovery {
|
|
|
246
282
|
if (rateLimitHit) {
|
|
247
283
|
rateLimitHitDuringSearch = true;
|
|
248
284
|
}
|
|
249
|
-
|
|
285
|
+
info(MODULE, `Found ${starredCandidates.length} candidates from starred repos`);
|
|
250
286
|
}
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
// Phase 2: General search (if still need more)
|
|
254
290
|
let phase2Error = null;
|
|
255
291
|
if (allCandidates.length < maxResults) {
|
|
256
|
-
|
|
292
|
+
info(MODULE, 'Phase 2: General issue search...');
|
|
257
293
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
258
294
|
try {
|
|
259
295
|
const { data } = await this.octokit.search.issuesAndPullRequests({
|
|
@@ -262,40 +298,9 @@ export class IssueDiscovery {
|
|
|
262
298
|
order: 'desc',
|
|
263
299
|
per_page: remainingNeeded * 3, // Fetch extra since some will be filtered
|
|
264
300
|
});
|
|
265
|
-
|
|
266
|
-
// Detect and filter label-farming repos (#97)
|
|
267
|
-
const spamRepos = detectLabelFarmingRepos(data.items);
|
|
268
|
-
if (spamRepos.size > 0) {
|
|
269
|
-
const spamCount = data.items.filter((i) => spamRepos.has(i.repository_url.split('/').slice(-2).join('/'))).length;
|
|
270
|
-
console.log(`[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(', ')}`);
|
|
271
|
-
}
|
|
272
|
-
// Filter and exclude already-found repos
|
|
301
|
+
info(MODULE, `Found ${data.total_count} issues in general search, processing top ${data.items.length}...`);
|
|
273
302
|
const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
|
|
274
|
-
const
|
|
275
|
-
.filter((item) => {
|
|
276
|
-
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
277
|
-
return !spamRepos.has(repoFullName);
|
|
278
|
-
})
|
|
279
|
-
.filter((item) => {
|
|
280
|
-
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
281
|
-
// Skip if already searched in earlier phases
|
|
282
|
-
return (!phase0RepoSet.has(repoFullName) && !starredRepoSet.has(repoFullName) && !seenRepos.has(repoFullName));
|
|
283
|
-
})
|
|
284
|
-
.slice(0, remainingNeeded * 2);
|
|
285
|
-
const { candidates: results, allFailed: allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, 'normal');
|
|
286
|
-
// Apply minStars filter to Phase 2 results (#105)
|
|
287
|
-
// Phase 0/1 are exempt — if you've already contributed to or starred a repo, star count is irrelevant.
|
|
288
|
-
const minStars = config.minStars ?? 50;
|
|
289
|
-
const starFiltered = results.filter((c) => {
|
|
290
|
-
if (c.projectHealth.checkFailed)
|
|
291
|
-
return true; // Don't penalize repos we couldn't check
|
|
292
|
-
const stars = c.projectHealth.stargazersCount ?? 0;
|
|
293
|
-
return stars >= minStars;
|
|
294
|
-
});
|
|
295
|
-
const starFilteredCount = results.length - starFiltered.length;
|
|
296
|
-
if (starFilteredCount > 0) {
|
|
297
|
-
console.log(`[STAR_FILTER] Filtered ${starFilteredCount} candidates below ${minStars} stars`);
|
|
298
|
-
}
|
|
303
|
+
const { candidates: starFiltered, allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.filterVetAndScore(data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], remainingNeeded, minStars, 'Phase 2');
|
|
299
304
|
allCandidates.push(...starFiltered);
|
|
300
305
|
if (allVetFailed) {
|
|
301
306
|
phase2Error = (phase2Error ? phase2Error + '; ' : '') + 'all vetting failed';
|
|
@@ -303,15 +308,15 @@ export class IssueDiscovery {
|
|
|
303
308
|
if (vetRateLimitHit) {
|
|
304
309
|
rateLimitHitDuringSearch = true;
|
|
305
310
|
}
|
|
306
|
-
|
|
311
|
+
info(MODULE, `Found ${starFiltered.length} candidates from general search`);
|
|
307
312
|
}
|
|
308
313
|
catch (error) {
|
|
309
|
-
const
|
|
310
|
-
phase2Error =
|
|
314
|
+
const errMsg = errorMessage(error);
|
|
315
|
+
phase2Error = errMsg;
|
|
311
316
|
if (IssueVetter.isRateLimitError(error)) {
|
|
312
317
|
rateLimitHitDuringSearch = true;
|
|
313
318
|
}
|
|
314
|
-
warn(MODULE, `Error in general issue search: ${
|
|
319
|
+
warn(MODULE, `Error in general issue search: ${errMsg}`);
|
|
315
320
|
}
|
|
316
321
|
}
|
|
317
322
|
// Phase 3: Actively maintained repos (#349)
|
|
@@ -320,13 +325,12 @@ export class IssueDiscovery {
|
|
|
320
325
|
// Uses label-free query to cast a wider net focused on repo health.
|
|
321
326
|
let phase3Error = null;
|
|
322
327
|
if (allCandidates.length < maxResults) {
|
|
323
|
-
|
|
328
|
+
info(MODULE, 'Phase 3: Searching actively maintained repos...');
|
|
324
329
|
const remainingNeeded = maxResults - allCandidates.length;
|
|
325
330
|
const thirtyDaysAgo = new Date();
|
|
326
331
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
327
332
|
const pushedSince = thirtyDaysAgo.toISOString().split('T')[0];
|
|
328
|
-
const
|
|
329
|
-
const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${phase3MinStars} pushed:>=${pushedSince} archived:false`
|
|
333
|
+
const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`
|
|
330
334
|
.replace(/ +/g, ' ')
|
|
331
335
|
.trim();
|
|
332
336
|
try {
|
|
@@ -336,36 +340,9 @@ export class IssueDiscovery {
|
|
|
336
340
|
order: 'desc',
|
|
337
341
|
per_page: remainingNeeded * 3,
|
|
338
342
|
});
|
|
339
|
-
|
|
340
|
-
// Filter spam, already-found repos, and already-searched repos
|
|
341
|
-
const spamRepos = detectLabelFarmingRepos(data.items);
|
|
342
|
-
if (spamRepos.size > 0) {
|
|
343
|
-
const spamCount = data.items.filter((i) => spamRepos.has(i.repository_url.split('/').slice(-2).join('/'))).length;
|
|
344
|
-
console.log(`[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(', ')}`);
|
|
345
|
-
}
|
|
343
|
+
info(MODULE, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
346
344
|
const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
|
|
347
|
-
const
|
|
348
|
-
.filter((item) => {
|
|
349
|
-
const repoFullName = item.repository_url.split('/').slice(-2).join('/');
|
|
350
|
-
return (!spamRepos.has(repoFullName) &&
|
|
351
|
-
!phase0RepoSet.has(repoFullName) &&
|
|
352
|
-
!starredRepoSet.has(repoFullName) &&
|
|
353
|
-
!seenRepos.has(repoFullName));
|
|
354
|
-
})
|
|
355
|
-
.slice(0, remainingNeeded * 2);
|
|
356
|
-
const { candidates: results, allFailed: allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, 'normal');
|
|
357
|
-
// Apply minStars filter (same as Phase 2, #105)
|
|
358
|
-
const minStars = config.minStars ?? 50;
|
|
359
|
-
const starFiltered = results.filter((c) => {
|
|
360
|
-
if (c.projectHealth.checkFailed)
|
|
361
|
-
return true;
|
|
362
|
-
const stars = c.projectHealth.stargazersCount ?? 0;
|
|
363
|
-
return stars >= minStars;
|
|
364
|
-
});
|
|
365
|
-
const starFilteredCount = results.length - starFiltered.length;
|
|
366
|
-
if (starFilteredCount > 0) {
|
|
367
|
-
console.log(`[STAR_FILTER] Filtered ${starFilteredCount} Phase 3 candidates below ${minStars} stars`);
|
|
368
|
-
}
|
|
345
|
+
const { candidates: starFiltered, allVetFailed, rateLimitHit: vetRateLimitHit, } = await this.filterVetAndScore(data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], remainingNeeded, minStars, 'Phase 3');
|
|
369
346
|
allCandidates.push(...starFiltered);
|
|
370
347
|
if (allVetFailed) {
|
|
371
348
|
phase3Error = 'all vetting failed';
|
|
@@ -373,15 +350,15 @@ export class IssueDiscovery {
|
|
|
373
350
|
if (vetRateLimitHit) {
|
|
374
351
|
rateLimitHitDuringSearch = true;
|
|
375
352
|
}
|
|
376
|
-
|
|
353
|
+
info(MODULE, `Found ${starFiltered.length} candidates from maintained-repo search`);
|
|
377
354
|
}
|
|
378
355
|
catch (error) {
|
|
379
|
-
const
|
|
380
|
-
phase3Error =
|
|
356
|
+
const errMsg = errorMessage(error);
|
|
357
|
+
phase3Error = errMsg;
|
|
381
358
|
if (IssueVetter.isRateLimitError(error)) {
|
|
382
359
|
rateLimitHitDuringSearch = true;
|
|
383
360
|
}
|
|
384
|
-
warn(MODULE, `Error in maintained-repo search: ${
|
|
361
|
+
warn(MODULE, `Error in maintained-repo search: ${errMsg}`);
|
|
385
362
|
}
|
|
386
363
|
}
|
|
387
364
|
if (allCandidates.length === 0) {
|
|
@@ -476,7 +453,7 @@ export class IssueDiscovery {
|
|
|
476
453
|
rateLimitFailures++;
|
|
477
454
|
}
|
|
478
455
|
const batchRepos = batch.join(', ');
|
|
479
|
-
warn(MODULE, `Error searching issues in batch [${batchRepos}]:`, error
|
|
456
|
+
warn(MODULE, `Error searching issues in batch [${batchRepos}]:`, errorMessage(error));
|
|
480
457
|
}
|
|
481
458
|
}
|
|
482
459
|
const allBatchesFailed = failedBatches === batches.length && batches.length > 0;
|
|
@@ -553,7 +530,7 @@ export class IssueDiscovery {
|
|
|
553
530
|
content += `- **Score**: Viability score (0-100)\n`;
|
|
554
531
|
content += `- **Recommendation**: Y = approve, N = skip, ? = needs_review\n`;
|
|
555
532
|
fs.writeFileSync(outputFile, content, 'utf-8');
|
|
556
|
-
|
|
533
|
+
info(MODULE, `Saved ${sorted.length} issues to ${outputFile}`);
|
|
557
534
|
return outputFile;
|
|
558
535
|
}
|
|
559
536
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { paginateAll } from './pagination.js';
|
|
8
8
|
import { parseGitHubUrl, daysBetween } from './utils.js';
|
|
9
|
-
import { ValidationError } from './errors.js';
|
|
9
|
+
import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
10
10
|
import { warn } from './logger.js';
|
|
11
11
|
import { getHttpCache, cachedRequest } from './http-cache.js';
|
|
12
12
|
import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
|
|
@@ -254,7 +254,7 @@ export class IssueVetter {
|
|
|
254
254
|
if (IssueVetter.isRateLimitError(error)) {
|
|
255
255
|
rateLimitFailures++;
|
|
256
256
|
}
|
|
257
|
-
warn(MODULE, `Error vetting issue ${url}:`, error
|
|
257
|
+
warn(MODULE, `Error vetting issue ${url}:`, errorMessage(error));
|
|
258
258
|
});
|
|
259
259
|
pending.push(task);
|
|
260
260
|
// Limit concurrency
|
|
@@ -275,11 +275,11 @@ export class IssueVetter {
|
|
|
275
275
|
}
|
|
276
276
|
/** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
|
|
277
277
|
static isRateLimitError(error) {
|
|
278
|
-
const status = error
|
|
278
|
+
const status = getHttpStatusCode(error);
|
|
279
279
|
if (status === 429)
|
|
280
280
|
return true;
|
|
281
281
|
if (status === 403) {
|
|
282
|
-
const msg =
|
|
282
|
+
const msg = errorMessage(error).toLowerCase();
|
|
283
283
|
return msg.includes('rate limit');
|
|
284
284
|
}
|
|
285
285
|
return false;
|
|
@@ -306,9 +306,9 @@ export class IssueVetter {
|
|
|
306
306
|
return { passed: data.total_count === 0 && linkedPRs.length === 0 };
|
|
307
307
|
}
|
|
308
308
|
catch (error) {
|
|
309
|
-
const
|
|
310
|
-
warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${
|
|
311
|
-
return { passed: true, inconclusive: true, reason:
|
|
309
|
+
const errMsg = errorMessage(error);
|
|
310
|
+
warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
311
|
+
return { passed: true, inconclusive: true, reason: errMsg };
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
/**
|
|
@@ -325,8 +325,8 @@ export class IssueVetter {
|
|
|
325
325
|
return data.total_count;
|
|
326
326
|
}
|
|
327
327
|
catch (error) {
|
|
328
|
-
const
|
|
329
|
-
warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${
|
|
328
|
+
const errMsg = errorMessage(error);
|
|
329
|
+
warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
330
330
|
return 0;
|
|
331
331
|
}
|
|
332
332
|
}
|
|
@@ -370,9 +370,9 @@ export class IssueVetter {
|
|
|
370
370
|
return { passed: true };
|
|
371
371
|
}
|
|
372
372
|
catch (error) {
|
|
373
|
-
const
|
|
374
|
-
warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${
|
|
375
|
-
return { passed: true, inconclusive: true, reason:
|
|
373
|
+
const errMsg = errorMessage(error);
|
|
374
|
+
warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
375
|
+
return { passed: true, inconclusive: true, reason: errMsg };
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
async checkProjectHealth(owner, repo) {
|
|
@@ -403,8 +403,8 @@ export class IssueVetter {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
catch (error) {
|
|
406
|
-
const
|
|
407
|
-
warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${
|
|
406
|
+
const errMsg = errorMessage(error);
|
|
407
|
+
warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${errMsg}. Defaulting to unknown.`);
|
|
408
408
|
}
|
|
409
409
|
return {
|
|
410
410
|
repo: `${owner}/${repo}`,
|
|
@@ -419,8 +419,8 @@ export class IssueVetter {
|
|
|
419
419
|
};
|
|
420
420
|
}
|
|
421
421
|
catch (error) {
|
|
422
|
-
const
|
|
423
|
-
warn(MODULE, `Error checking project health for ${owner}/${repo}: ${
|
|
422
|
+
const errMsg = errorMessage(error);
|
|
423
|
+
warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
424
424
|
return {
|
|
425
425
|
repo: `${owner}/${repo}`,
|
|
426
426
|
lastCommitAt: '',
|
|
@@ -430,7 +430,7 @@ export class IssueVetter {
|
|
|
430
430
|
ciStatus: 'unknown',
|
|
431
431
|
isActive: false,
|
|
432
432
|
checkFailed: true,
|
|
433
|
-
failureReason:
|
|
433
|
+
failureReason: errMsg,
|
|
434
434
|
};
|
|
435
435
|
}
|
|
436
436
|
}
|
package/dist/core/logger.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export declare function isDebugEnabled(): boolean;
|
|
|
11
11
|
* Log a debug message. Only outputs when --debug is enabled.
|
|
12
12
|
*/
|
|
13
13
|
export declare function debug(module: string, message: string, ...args: unknown[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Log an informational message. Always outputs to stderr.
|
|
16
|
+
* Use for user-facing progress indicators during long-running operations.
|
|
17
|
+
*/
|
|
18
|
+
export declare function info(module: string, message: string, ...args: unknown[]): void;
|
|
14
19
|
/**
|
|
15
20
|
* Log a warning. Always outputs.
|
|
16
21
|
*/
|
package/dist/core/logger.js
CHANGED
|
@@ -21,6 +21,14 @@ export function debug(module, message, ...args) {
|
|
|
21
21
|
const timestamp = new Date().toISOString();
|
|
22
22
|
console.error(`[${timestamp}] [DEBUG] [${module}] ${message}`, ...args);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Log an informational message. Always outputs to stderr.
|
|
26
|
+
* Use for user-facing progress indicators during long-running operations.
|
|
27
|
+
*/
|
|
28
|
+
export function info(module, message, ...args) {
|
|
29
|
+
const timestamp = new Date().toISOString();
|
|
30
|
+
console.error(`[${timestamp}] [INFO] [${module}] ${message}`, ...args);
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* Log a warning. Always outputs.
|
|
26
34
|
*/
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - github-stats.ts: Merged/closed PR counts and star fetching
|
|
13
13
|
*/
|
|
14
14
|
import { FetchedPR, DailyDigest, ClosedPR, MergedPR } from './types.js';
|
|
15
|
+
import { type PRCountsResult } from './github-stats.js';
|
|
15
16
|
export { computeDisplayLabel } from './display-utils.js';
|
|
16
17
|
export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
|
|
17
18
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
@@ -59,35 +60,20 @@ export declare class PRMonitor {
|
|
|
59
60
|
* Fetch merged PR counts and latest merge dates per repository for the configured user.
|
|
60
61
|
* Delegates to github-stats module.
|
|
61
62
|
*/
|
|
62
|
-
fetchUserMergedPRCounts(): Promise<{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}>;
|
|
67
|
-
monthlyCounts: Record<string, number>;
|
|
68
|
-
monthlyOpenedCounts: Record<string, number>;
|
|
69
|
-
dailyActivityCounts: Record<string, number>;
|
|
70
|
-
}>;
|
|
63
|
+
fetchUserMergedPRCounts(): Promise<PRCountsResult<{
|
|
64
|
+
count: number;
|
|
65
|
+
lastMergedAt: string;
|
|
66
|
+
}>>;
|
|
71
67
|
/**
|
|
72
68
|
* Fetch closed-without-merge PR counts per repository for the configured user.
|
|
73
69
|
* Delegates to github-stats module.
|
|
74
70
|
*/
|
|
75
|
-
fetchUserClosedPRCounts(): Promise<
|
|
76
|
-
repos: Map<string, number>;
|
|
77
|
-
monthlyCounts: Record<string, number>;
|
|
78
|
-
monthlyOpenedCounts: Record<string, number>;
|
|
79
|
-
dailyActivityCounts: Record<string, number>;
|
|
80
|
-
}>;
|
|
71
|
+
fetchUserClosedPRCounts(): Promise<PRCountsResult<number>>;
|
|
81
72
|
/**
|
|
82
73
|
* Fetch GitHub star counts for a list of repositories.
|
|
83
74
|
* Delegates to github-stats module.
|
|
84
75
|
*/
|
|
85
76
|
fetchRepoStarCounts(repos: string[]): Promise<Map<string, number>>;
|
|
86
|
-
/**
|
|
87
|
-
* Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
|
|
88
|
-
* Returns parsed search results that pass all filters.
|
|
89
|
-
*/
|
|
90
|
-
private fetchRecentPRs;
|
|
91
77
|
/**
|
|
92
78
|
* Fetch PRs closed without merge in the last N days.
|
|
93
79
|
* Delegates to github-stats module.
|