@oss-scout/core 0.2.1 → 0.4.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.
- package/LICENSE +21 -0
- package/dist/cli.bundle.cjs +42 -38
- package/dist/cli.js +108 -1
- package/dist/commands/config.d.ts +1 -3
- package/dist/commands/config.js +7 -7
- package/dist/commands/setup.js +3 -9
- package/dist/commands/skip.d.ts +33 -0
- package/dist/commands/skip.js +89 -0
- package/dist/core/bootstrap.js +2 -2
- package/dist/core/gist-state-store.js +19 -1
- package/dist/core/issue-discovery.d.ts +3 -3
- package/dist/core/issue-discovery.js +90 -94
- package/dist/core/issue-vetting.d.ts +0 -2
- package/dist/core/issue-vetting.js +0 -4
- package/dist/core/local-state.js +2 -2
- package/dist/core/schemas.d.ts +22 -8
- package/dist/core/schemas.js +13 -4
- package/dist/core/search-phases.d.ts +21 -0
- package/dist/core/search-phases.js +157 -11
- package/dist/core/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/scout.d.ts +27 -2
- package/dist/scout.js +69 -3
- package/package.json +17 -14
- package/dist/core/concurrency.d.ts +0 -6
- package/dist/core/concurrency.js +0 -25
|
@@ -21,10 +21,8 @@ import { debug, info, warn } from "./logger.js";
|
|
|
21
21
|
import { isDocOnlyIssue, applyPerRepoCap, } from "./issue-filtering.js";
|
|
22
22
|
import { IssueVetter } from "./issue-vetting.js";
|
|
23
23
|
import { getTopicsForCategories } from "./category-mapping.js";
|
|
24
|
-
import { buildEffectiveLabels, interleaveArrays, cachedSearchIssues, filterVetAndScore,
|
|
24
|
+
import { buildEffectiveLabels, interleaveArrays, cachedSearchIssues, fetchIssuesFromMaintainedRepos, filterVetAndScore, fetchIssuesFromKnownRepos, searchWithChunkedLabels, } from "./search-phases.js";
|
|
25
25
|
const MODULE = "issue-discovery";
|
|
26
|
-
/** Delay between major search phases to let GitHub's rate limit window cool down. */
|
|
27
|
-
const INTER_PHASE_DELAY_MS = 2000;
|
|
28
26
|
/** If remaining search quota is below this, skip heavy phases (2, 3). */
|
|
29
27
|
const LOW_BUDGET_THRESHOLD = 20;
|
|
30
28
|
/** If remaining search quota is below this, only run Phase 0. */
|
|
@@ -47,6 +45,8 @@ function buildIssueFilter(config) {
|
|
|
47
45
|
return false;
|
|
48
46
|
if (config.lowScoringRepos.has(repoFullName))
|
|
49
47
|
return false;
|
|
48
|
+
if (config.skippedUrls.has(item.html_url))
|
|
49
|
+
return false;
|
|
50
50
|
const updatedAt = new Date(item.updated_at);
|
|
51
51
|
const ageDays = daysBetween(updatedAt, config.now);
|
|
52
52
|
if (ageDays > config.maxAgeDays)
|
|
@@ -58,63 +58,27 @@ function buildIssueFilter(config) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
/** Phase 0: Search repos where user has merged PRs (highest merge probability). */
|
|
61
|
-
async function runPhase0(octokit, vetter, repos,
|
|
61
|
+
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
62
62
|
info(MODULE, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
63
|
-
const { candidates,
|
|
63
|
+
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
64
64
|
info(MODULE, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
65
65
|
return {
|
|
66
66
|
candidates,
|
|
67
|
-
error:
|
|
67
|
+
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
68
68
|
rateLimitHit,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
/** Phase 0.5: Search preferred organizations. */
|
|
72
|
-
async function runPhase05(octokit, vetter, orgsToSearch, baseQualifiers, labels, maxResults, phase0RepoSet, filterIssues) {
|
|
73
|
-
info(MODULE, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
|
|
74
|
-
const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
|
|
75
|
-
const orgOps = orgsToSearch.length - 1;
|
|
76
|
-
try {
|
|
77
|
-
const allItems = await searchWithChunkedLabels(octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`
|
|
78
|
-
.replace(/ +/g, " ")
|
|
79
|
-
.trim(), maxResults * 3);
|
|
80
|
-
if (allItems.length === 0) {
|
|
81
|
-
return { candidates: [], error: null, rateLimitHit: false };
|
|
82
|
-
}
|
|
83
|
-
const filtered = filterIssues(allItems).filter((item) => {
|
|
84
|
-
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
85
|
-
if (!repoFullName)
|
|
86
|
-
return false;
|
|
87
|
-
return !phase0RepoSet.has(repoFullName);
|
|
88
|
-
});
|
|
89
|
-
const { candidates, allFailed: allVetFailed, rateLimitHit, } = await vetter.vetIssuesParallel(filtered.slice(0, maxResults * 2).map((i) => i.html_url), maxResults, "preferred_org");
|
|
90
|
-
info(MODULE, `Found ${candidates.length} candidates from preferred orgs`);
|
|
91
|
-
return {
|
|
92
|
-
candidates,
|
|
93
|
-
error: allVetFailed ? "All preferred org issue vetting failed" : null,
|
|
94
|
-
rateLimitHit,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
const errMsg = errorMessage(error);
|
|
99
|
-
warn(MODULE, `Error searching preferred orgs: ${errMsg}`);
|
|
100
|
-
return {
|
|
101
|
-
candidates: [],
|
|
102
|
-
error: errMsg,
|
|
103
|
-
rateLimitHit: isRateLimitError(error),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
71
|
/** Phase 1: Search starred repos. */
|
|
108
|
-
async function runPhase1(octokit, vetter, repos,
|
|
72
|
+
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
109
73
|
info(MODULE, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
110
|
-
// Cap labels
|
|
111
|
-
// interest, so fewer labels suffice.
|
|
74
|
+
// Cap labels: starred repos already signal user interest, so fewer labels suffice.
|
|
112
75
|
const phase1Labels = labels.slice(0, 3);
|
|
113
|
-
const
|
|
76
|
+
const reposToSearch = repos.slice(0, 10);
|
|
77
|
+
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
114
78
|
info(MODULE, `Found ${candidates.length} candidates from starred repos`);
|
|
115
79
|
return {
|
|
116
80
|
candidates,
|
|
117
|
-
error:
|
|
81
|
+
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
118
82
|
rateLimitHit,
|
|
119
83
|
};
|
|
120
84
|
}
|
|
@@ -184,9 +148,36 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
184
148
|
rateLimitHit,
|
|
185
149
|
};
|
|
186
150
|
}
|
|
187
|
-
/** Phase 3: Actively maintained repos. */
|
|
188
|
-
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
151
|
+
/** Phase 3: Actively maintained repos (REST-first, Search API fallback). */
|
|
152
|
+
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
189
153
|
info(MODULE, "Phase 3: Searching actively maintained repos...");
|
|
154
|
+
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
155
|
+
// Step 1: Try REST API with starred repos first (no Search API quota used)
|
|
156
|
+
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
157
|
+
if (eligibleStarred.length > 0) {
|
|
158
|
+
info(MODULE, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
159
|
+
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
160
|
+
if (restItems.length > 0) {
|
|
161
|
+
try {
|
|
162
|
+
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit, } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
163
|
+
if (candidates.length > 0) {
|
|
164
|
+
info(MODULE, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
165
|
+
return {
|
|
166
|
+
candidates,
|
|
167
|
+
error: allVetFailed ? "all vetting failed" : null,
|
|
168
|
+
rateLimitHit: vetRateLimitHit,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (getHttpStatusCode(error) === 401)
|
|
174
|
+
throw error;
|
|
175
|
+
warn(MODULE, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Step 2: Fall back to Search API if REST didn't yield results
|
|
180
|
+
info(MODULE, "Phase 3: Falling back to Search API...");
|
|
190
181
|
const thirtyDaysAgo = new Date();
|
|
191
182
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
192
183
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -203,7 +194,6 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
203
194
|
per_page: maxResults * 3,
|
|
204
195
|
});
|
|
205
196
|
info(MODULE, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
206
|
-
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
207
197
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit, } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
208
198
|
info(MODULE, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
209
199
|
return {
|
|
@@ -228,7 +218,6 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
228
218
|
*
|
|
229
219
|
* Search phases (in priority order):
|
|
230
220
|
* 0. Repos where user has merged PRs (highest merge probability)
|
|
231
|
-
* 0.5. Preferred organizations
|
|
232
221
|
* 1. Starred repos
|
|
233
222
|
* 2. General label-filtered search
|
|
234
223
|
* 3. Actively maintained repos
|
|
@@ -264,8 +253,8 @@ export class IssueDiscovery {
|
|
|
264
253
|
}
|
|
265
254
|
/**
|
|
266
255
|
* Search for issues matching our criteria.
|
|
267
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
268
|
-
*
|
|
256
|
+
* Searches in priority order: merged-PR repos first (no label filter), then starred
|
|
257
|
+
* repos, then general search, then actively maintained repos.
|
|
269
258
|
* Filters out issues from low-scoring and excluded repos.
|
|
270
259
|
*
|
|
271
260
|
* @param options - Search configuration
|
|
@@ -294,6 +283,7 @@ export class IssueDiscovery {
|
|
|
294
283
|
(scopes ? buildEffectiveLabels(scopes, config.labels) : config.labels);
|
|
295
284
|
const maxResults = options.maxResults || 10;
|
|
296
285
|
const minStars = config.minStars ?? 50;
|
|
286
|
+
const interPhaseDelay = config.interPhaseDelayMs ?? 30000;
|
|
297
287
|
// Strategy selection
|
|
298
288
|
const ALL_STRATEGIES = CONCRETE_STRATEGIES;
|
|
299
289
|
const rawStrategies = options.strategies ??
|
|
@@ -329,6 +319,11 @@ export class IssueDiscovery {
|
|
|
329
319
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 60000).toISOString());
|
|
330
320
|
warn(MODULE, "Could not check rate limit — using conservative budget, skipping heavy phases:", errorMessage(error));
|
|
331
321
|
}
|
|
322
|
+
if (searchBudget <= 0) {
|
|
323
|
+
this.rateLimitWarning =
|
|
324
|
+
"GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
325
|
+
return { candidates: [], strategiesUsed: [] };
|
|
326
|
+
}
|
|
332
327
|
// Derive search context
|
|
333
328
|
const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
|
|
334
329
|
const starredRepos = this.getStarredRepos();
|
|
@@ -352,6 +347,7 @@ export class IssueDiscovery {
|
|
|
352
347
|
excludeOrgs: new Set((config.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
353
348
|
aiBlocklisted,
|
|
354
349
|
lowScoringRepos,
|
|
350
|
+
skippedUrls: options.skippedUrls ?? new Set(),
|
|
355
351
|
maxAgeDays: config.maxIssueAgeDays || 90,
|
|
356
352
|
now: new Date(),
|
|
357
353
|
includeDocIssues: config.includeDocIssues ?? true,
|
|
@@ -362,7 +358,7 @@ export class IssueDiscovery {
|
|
|
362
358
|
if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
|
|
363
359
|
const remaining = maxResults - allCandidates.length;
|
|
364
360
|
if (remaining > 0) {
|
|
365
|
-
const result = await runPhase0(this.octokit, this.vetter, phase0Repos,
|
|
361
|
+
const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
|
|
366
362
|
allCandidates.push(...result.candidates);
|
|
367
363
|
phaseErrors["0"] = result.error;
|
|
368
364
|
if (result.rateLimitHit)
|
|
@@ -370,39 +366,20 @@ export class IssueDiscovery {
|
|
|
370
366
|
}
|
|
371
367
|
strategiesUsed.push("merged");
|
|
372
368
|
}
|
|
373
|
-
// Phase 0.5: Preferred organizations
|
|
374
|
-
const preferredOrgs = config.preferredOrgs ?? [];
|
|
375
|
-
if (allCandidates.length < maxResults &&
|
|
376
|
-
preferredOrgs.length > 0 &&
|
|
377
|
-
searchBudget >= CRITICAL_BUDGET_THRESHOLD &&
|
|
378
|
-
enabledStrategies.has("orgs")) {
|
|
379
|
-
if (phase0Repos.length > 0)
|
|
380
|
-
await sleep(INTER_PHASE_DELAY_MS);
|
|
381
|
-
const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
|
|
382
|
-
const orgsToSearch = preferredOrgs
|
|
383
|
-
.filter((org) => !phase0Orgs.has(org.toLowerCase()))
|
|
384
|
-
.slice(0, 5);
|
|
385
|
-
if (orgsToSearch.length > 0) {
|
|
386
|
-
const remaining = maxResults - allCandidates.length;
|
|
387
|
-
const result = await runPhase05(this.octokit, this.vetter, orgsToSearch, baseQualifiers, labels, remaining, phase0RepoSet, filterIssues);
|
|
388
|
-
allCandidates.push(...result.candidates);
|
|
389
|
-
phaseErrors["0.5"] = result.error;
|
|
390
|
-
if (result.rateLimitHit)
|
|
391
|
-
rateLimitHitDuringSearch = true;
|
|
392
|
-
}
|
|
393
|
-
strategiesUsed.push("orgs");
|
|
394
|
-
}
|
|
395
369
|
// Phase 1: Starred repos
|
|
396
370
|
if (allCandidates.length < maxResults &&
|
|
397
371
|
starredRepos.length > 0 &&
|
|
398
372
|
searchBudget >= CRITICAL_BUDGET_THRESHOLD &&
|
|
399
373
|
enabledStrategies.has("starred")) {
|
|
400
|
-
|
|
374
|
+
if (interPhaseDelay > 0) {
|
|
375
|
+
info(MODULE, `Waiting ${(interPhaseDelay / 1000).toFixed(0)}s between phases for rate limit management...`);
|
|
376
|
+
await sleep(interPhaseDelay);
|
|
377
|
+
}
|
|
401
378
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
402
379
|
if (reposToSearch.length > 0) {
|
|
403
380
|
const remaining = maxResults - allCandidates.length;
|
|
404
381
|
if (remaining > 0) {
|
|
405
|
-
const result = await runPhase1(this.octokit, this.vetter, reposToSearch,
|
|
382
|
+
const result = await runPhase1(this.octokit, this.vetter, reposToSearch, labels, remaining, filterIssues);
|
|
406
383
|
allCandidates.push(...result.candidates);
|
|
407
384
|
phaseErrors["1"] = result.error;
|
|
408
385
|
if (result.rateLimitHit)
|
|
@@ -411,26 +388,49 @@ export class IssueDiscovery {
|
|
|
411
388
|
}
|
|
412
389
|
strategiesUsed.push("starred");
|
|
413
390
|
}
|
|
414
|
-
// Phase 2: General search
|
|
391
|
+
// Phase 2: General search (with rate limit mitigation)
|
|
392
|
+
const broadDelay = config.broadPhaseDelayMs ?? 90000;
|
|
393
|
+
const skipThreshold = config.skipBroadWhenSufficientResults ?? 15;
|
|
415
394
|
if (allCandidates.length < maxResults &&
|
|
416
395
|
searchBudget >= LOW_BUDGET_THRESHOLD &&
|
|
417
396
|
enabledStrategies.has("broad")) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
397
|
+
// Skip broad search if we already have enough candidates
|
|
398
|
+
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
399
|
+
info(MODULE, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// Always apply baseline inter-phase delay
|
|
403
|
+
if (interPhaseDelay > 0) {
|
|
404
|
+
info(MODULE, `Waiting ${(interPhaseDelay / 1000).toFixed(0)}s between phases for rate limit management...`);
|
|
405
|
+
await sleep(interPhaseDelay);
|
|
406
|
+
}
|
|
407
|
+
// Apply additional broad-phase cooldown, but skip if previous phases found nothing
|
|
408
|
+
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
409
|
+
info(MODULE, `Waiting ${(broadDelay / 1000).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
410
|
+
await sleep(broadDelay);
|
|
411
|
+
}
|
|
412
|
+
else if (allCandidates.length === 0) {
|
|
413
|
+
info(MODULE, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
414
|
+
}
|
|
415
|
+
const remaining = maxResults - allCandidates.length;
|
|
416
|
+
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
417
|
+
allCandidates.push(...result.candidates);
|
|
418
|
+
phaseErrors["2"] = result.error;
|
|
419
|
+
if (result.rateLimitHit)
|
|
420
|
+
rateLimitHitDuringSearch = true;
|
|
421
|
+
}
|
|
425
422
|
strategiesUsed.push("broad");
|
|
426
423
|
}
|
|
427
424
|
// Phase 3: Actively maintained repos
|
|
428
425
|
if (allCandidates.length < maxResults &&
|
|
429
426
|
searchBudget >= LOW_BUDGET_THRESHOLD &&
|
|
430
427
|
enabledStrategies.has("maintained")) {
|
|
431
|
-
|
|
428
|
+
if (interPhaseDelay > 0) {
|
|
429
|
+
info(MODULE, `Waiting ${(interPhaseDelay / 1000).toFixed(0)}s between phases for rate limit management...`);
|
|
430
|
+
await sleep(interPhaseDelay);
|
|
431
|
+
}
|
|
432
432
|
const remaining = maxResults - allCandidates.length;
|
|
433
|
-
const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
433
|
+
const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues);
|
|
434
434
|
allCandidates.push(...result.candidates);
|
|
435
435
|
phaseErrors["3"] = result.error;
|
|
436
436
|
if (result.rateLimitHit)
|
|
@@ -451,9 +451,6 @@ export class IssueDiscovery {
|
|
|
451
451
|
phaseErrors["0"]
|
|
452
452
|
? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}`
|
|
453
453
|
: null,
|
|
454
|
-
phaseErrors["0.5"]
|
|
455
|
-
? `Phase 0.5 (preferred orgs): ${phaseErrors["0.5"]}`
|
|
456
|
-
: null,
|
|
457
454
|
phaseErrors["1"]
|
|
458
455
|
? `Phase 1 (starred repos): ${phaseErrors["1"]}`
|
|
459
456
|
: null,
|
|
@@ -482,9 +479,8 @@ export class IssueDiscovery {
|
|
|
482
479
|
allCandidates.sort((a, b) => {
|
|
483
480
|
const priorityOrder = {
|
|
484
481
|
merged_pr: 0,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
normal: 3,
|
|
482
|
+
starred: 1,
|
|
483
|
+
normal: 2,
|
|
488
484
|
};
|
|
489
485
|
const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
|
|
490
486
|
if (priorityDiff !== 0)
|
|
@@ -17,8 +17,6 @@ export interface ScoutStateReader {
|
|
|
17
17
|
getReposWithMergedPRs(): string[];
|
|
18
18
|
/** User's starred repos (from GitHub). */
|
|
19
19
|
getStarredRepos(): string[];
|
|
20
|
-
/** Preferred GitHub orgs from user preferences. */
|
|
21
|
-
getPreferredOrgs(): string[];
|
|
22
20
|
/** Preferred project categories from user preferences. */
|
|
23
21
|
getProjectCategories(): ProjectCategory[];
|
|
24
22
|
/** Numeric quality score for a repo, or null if not evaluated. */
|
|
@@ -205,14 +205,10 @@ export class IssueVetter {
|
|
|
205
205
|
matchesPreferredCategory: matchesCategory,
|
|
206
206
|
});
|
|
207
207
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
208
|
-
const preferredOrgs = this.stateReader.getPreferredOrgs();
|
|
209
208
|
let searchPriority = "normal";
|
|
210
209
|
if (effectiveMergedCount > 0) {
|
|
211
210
|
searchPriority = "merged_pr";
|
|
212
211
|
}
|
|
213
|
-
else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
|
|
214
|
-
searchPriority = "preferred_org";
|
|
215
|
-
}
|
|
216
212
|
else if (starredRepos.includes(repoFullName)) {
|
|
217
213
|
searchPriority = "starred";
|
|
218
214
|
}
|
package/dist/core/local-state.js
CHANGED
|
@@ -39,8 +39,8 @@ export function loadLocalState() {
|
|
|
39
39
|
fs.copyFileSync(statePath, backupPath);
|
|
40
40
|
warn(MODULE, `Corrupt state backed up to ${backupPath}`);
|
|
41
41
|
}
|
|
42
|
-
catch {
|
|
43
|
-
|
|
42
|
+
catch (backupErr) {
|
|
43
|
+
warn(MODULE, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
44
44
|
}
|
|
45
45
|
return ScoutStateSchema.parse({ version: 1 });
|
|
46
46
|
}
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -27,13 +27,12 @@ export declare const IssueScopeSchema: z.ZodEnum<{
|
|
|
27
27
|
export declare const SearchStrategySchema: z.ZodEnum<{
|
|
28
28
|
all: "all";
|
|
29
29
|
merged: "merged";
|
|
30
|
-
orgs: "orgs";
|
|
31
30
|
starred: "starred";
|
|
32
31
|
broad: "broad";
|
|
33
32
|
maintained: "maintained";
|
|
34
33
|
}>;
|
|
35
34
|
/** All concrete strategies (excludes 'all' meta-strategy). */
|
|
36
|
-
export declare const CONCRETE_STRATEGIES: readonly ["merged", "
|
|
35
|
+
export declare const CONCRETE_STRATEGIES: readonly ["merged", "starred", "broad", "maintained"];
|
|
37
36
|
export declare const RepoSignalsSchema: z.ZodObject<{
|
|
38
37
|
hasActiveMaintainers: z.ZodBoolean;
|
|
39
38
|
isResponsive: z.ZodBoolean;
|
|
@@ -152,6 +151,13 @@ export declare const TrackedIssueSchema: z.ZodObject<{
|
|
|
152
151
|
notes: z.ZodArray<z.ZodString>;
|
|
153
152
|
}, z.core.$strip>>;
|
|
154
153
|
}, z.core.$strip>;
|
|
154
|
+
export declare const SkippedIssueSchema: z.ZodObject<{
|
|
155
|
+
url: z.ZodString;
|
|
156
|
+
repo: z.ZodString;
|
|
157
|
+
number: z.ZodNumber;
|
|
158
|
+
title: z.ZodString;
|
|
159
|
+
skippedAt: z.ZodString;
|
|
160
|
+
}, z.core.$strip>;
|
|
155
161
|
export declare const SavedCandidateSchema: z.ZodObject<{
|
|
156
162
|
issueUrl: z.ZodString;
|
|
157
163
|
repo: z.ZodString;
|
|
@@ -185,7 +191,6 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
185
191
|
excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
186
192
|
excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
187
193
|
aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
188
|
-
preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
189
194
|
projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
190
195
|
nonprofit: "nonprofit";
|
|
191
196
|
devtools: "devtools";
|
|
@@ -198,6 +203,7 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
198
203
|
maxIssueAgeDays: z.ZodDefault<z.ZodNumber>;
|
|
199
204
|
includeDocIssues: z.ZodDefault<z.ZodBoolean>;
|
|
200
205
|
minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
|
|
206
|
+
interPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
201
207
|
persistence: z.ZodDefault<z.ZodEnum<{
|
|
202
208
|
local: "local";
|
|
203
209
|
gist: "gist";
|
|
@@ -205,11 +211,12 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
205
211
|
defaultStrategy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
206
212
|
all: "all";
|
|
207
213
|
merged: "merged";
|
|
208
|
-
orgs: "orgs";
|
|
209
214
|
starred: "starred";
|
|
210
215
|
broad: "broad";
|
|
211
216
|
maintained: "maintained";
|
|
212
217
|
}>>>;
|
|
218
|
+
broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
219
|
+
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
213
220
|
}, z.core.$strip>;
|
|
214
221
|
export declare const ScoutStateSchema: z.ZodObject<{
|
|
215
222
|
version: z.ZodLiteral<1>;
|
|
@@ -225,7 +232,6 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
225
232
|
excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
226
233
|
excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
227
234
|
aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
228
|
-
preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
229
235
|
projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
230
236
|
nonprofit: "nonprofit";
|
|
231
237
|
devtools: "devtools";
|
|
@@ -238,6 +244,7 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
238
244
|
maxIssueAgeDays: z.ZodDefault<z.ZodNumber>;
|
|
239
245
|
includeDocIssues: z.ZodDefault<z.ZodBoolean>;
|
|
240
246
|
minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
|
|
247
|
+
interPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
241
248
|
persistence: z.ZodDefault<z.ZodEnum<{
|
|
242
249
|
local: "local";
|
|
243
250
|
gist: "gist";
|
|
@@ -245,11 +252,12 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
245
252
|
defaultStrategy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
246
253
|
all: "all";
|
|
247
254
|
merged: "merged";
|
|
248
|
-
orgs: "orgs";
|
|
249
255
|
starred: "starred";
|
|
250
256
|
broad: "broad";
|
|
251
257
|
maintained: "maintained";
|
|
252
258
|
}>>>;
|
|
259
|
+
broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
260
|
+
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
253
261
|
}, z.core.$strip>>;
|
|
254
262
|
repoScores: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
255
263
|
repo: z.ZodString;
|
|
@@ -296,11 +304,17 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
296
304
|
lastSeenAt: z.ZodString;
|
|
297
305
|
lastScore: z.ZodNumber;
|
|
298
306
|
}, z.core.$strip>>>;
|
|
307
|
+
skippedIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
308
|
+
url: z.ZodString;
|
|
309
|
+
repo: z.ZodString;
|
|
310
|
+
number: z.ZodNumber;
|
|
311
|
+
title: z.ZodString;
|
|
312
|
+
skippedAt: z.ZodString;
|
|
313
|
+
}, z.core.$strip>>>;
|
|
299
314
|
lastSearchAt: z.ZodOptional<z.ZodString>;
|
|
300
315
|
lastRunAt: z.ZodDefault<z.ZodString>;
|
|
301
316
|
gistId: z.ZodOptional<z.ZodString>;
|
|
302
317
|
}, z.core.$strip>;
|
|
303
|
-
export type IssueStatus = z.infer<typeof IssueStatusSchema>;
|
|
304
318
|
export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
|
|
305
319
|
export type IssueScope = z.infer<typeof IssueScopeSchema>;
|
|
306
320
|
export type SearchStrategy = z.infer<typeof SearchStrategySchema>;
|
|
@@ -311,7 +325,7 @@ export type StoredClosedPR = z.infer<typeof StoredClosedPRSchema>;
|
|
|
311
325
|
export type ContributionGuidelines = z.infer<typeof ContributionGuidelinesSchema>;
|
|
312
326
|
export type IssueVettingResult = z.infer<typeof IssueVettingResultSchema>;
|
|
313
327
|
export type TrackedIssue = z.infer<typeof TrackedIssueSchema>;
|
|
314
|
-
export type PersistenceMode = z.infer<typeof PersistenceModeSchema>;
|
|
315
328
|
export type ScoutPreferences = z.infer<typeof ScoutPreferencesSchema>;
|
|
316
329
|
export type SavedCandidate = z.infer<typeof SavedCandidateSchema>;
|
|
330
|
+
export type SkippedIssue = z.infer<typeof SkippedIssueSchema>;
|
|
317
331
|
export type ScoutState = z.infer<typeof ScoutStateSchema>;
|
package/dist/core/schemas.js
CHANGED
|
@@ -27,7 +27,6 @@ export const IssueScopeSchema = z.enum([
|
|
|
27
27
|
]);
|
|
28
28
|
export const SearchStrategySchema = z.enum([
|
|
29
29
|
"merged",
|
|
30
|
-
"orgs",
|
|
31
30
|
"starred",
|
|
32
31
|
"broad",
|
|
33
32
|
"maintained",
|
|
@@ -36,7 +35,6 @@ export const SearchStrategySchema = z.enum([
|
|
|
36
35
|
/** All concrete strategies (excludes 'all' meta-strategy). */
|
|
37
36
|
export const CONCRETE_STRATEGIES = [
|
|
38
37
|
"merged",
|
|
39
|
-
"orgs",
|
|
40
38
|
"starred",
|
|
41
39
|
"broad",
|
|
42
40
|
"maintained",
|
|
@@ -111,6 +109,14 @@ export const TrackedIssueSchema = z.object({
|
|
|
111
109
|
vetted: z.boolean(),
|
|
112
110
|
vettingResult: IssueVettingResultSchema.optional(),
|
|
113
111
|
});
|
|
112
|
+
// ── Skipped issue schema ──────────────────────────────────────────
|
|
113
|
+
export const SkippedIssueSchema = z.object({
|
|
114
|
+
url: z.string(),
|
|
115
|
+
repo: z.string(),
|
|
116
|
+
number: z.number(),
|
|
117
|
+
title: z.string(),
|
|
118
|
+
skippedAt: z.string(),
|
|
119
|
+
});
|
|
114
120
|
// ── Saved candidate schema ─────────────────────────────────────────
|
|
115
121
|
export const SavedCandidateSchema = z.object({
|
|
116
122
|
issueUrl: z.string(),
|
|
@@ -129,20 +135,22 @@ export const SavedCandidateSchema = z.object({
|
|
|
129
135
|
export const PersistenceModeSchema = z.enum(["local", "gist"]);
|
|
130
136
|
export const ScoutPreferencesSchema = z.object({
|
|
131
137
|
githubUsername: z.string().default(""),
|
|
132
|
-
languages: z.array(z.string()).default(["
|
|
138
|
+
languages: z.array(z.string()).default(["any"]),
|
|
133
139
|
labels: z.array(z.string()).default(["good first issue", "help wanted"]),
|
|
134
140
|
scope: z.array(IssueScopeSchema).optional(),
|
|
135
141
|
excludeRepos: z.array(z.string()).default([]),
|
|
136
142
|
excludeOrgs: z.array(z.string()).default([]),
|
|
137
143
|
aiPolicyBlocklist: z.array(z.string()).default(["matplotlib/matplotlib"]),
|
|
138
|
-
preferredOrgs: z.array(z.string()).default([]),
|
|
139
144
|
projectCategories: z.array(ProjectCategorySchema).default([]),
|
|
140
145
|
minStars: z.number().default(50),
|
|
141
146
|
maxIssueAgeDays: z.number().default(90),
|
|
142
147
|
includeDocIssues: z.boolean().default(true),
|
|
143
148
|
minRepoScoreThreshold: z.number().default(4),
|
|
149
|
+
interPhaseDelayMs: z.number().min(0).max(120000).default(30000),
|
|
144
150
|
persistence: PersistenceModeSchema.default("local"),
|
|
145
151
|
defaultStrategy: z.array(SearchStrategySchema).optional(),
|
|
152
|
+
broadPhaseDelayMs: z.number().min(0).max(300000).default(90000),
|
|
153
|
+
skipBroadWhenSufficientResults: z.number().int().min(0).max(100).default(15),
|
|
146
154
|
});
|
|
147
155
|
// ── Root state schema ───────────────────────────────────────────────
|
|
148
156
|
export const ScoutStateSchema = z.object({
|
|
@@ -154,6 +162,7 @@ export const ScoutStateSchema = z.object({
|
|
|
154
162
|
mergedPRs: z.array(StoredMergedPRSchema).default([]),
|
|
155
163
|
closedPRs: z.array(StoredClosedPRSchema).default([]),
|
|
156
164
|
savedResults: z.array(SavedCandidateSchema).default([]),
|
|
165
|
+
skippedIssues: z.array(SkippedIssueSchema).default([]),
|
|
157
166
|
lastSearchAt: z.string().optional(),
|
|
158
167
|
lastRunAt: z.string().default(() => new Date().toISOString()),
|
|
159
168
|
gistId: z.string().optional(),
|
|
@@ -26,6 +26,27 @@ export declare function cachedSearchIssues(octokit: Octokit, params: {
|
|
|
26
26
|
total_count: number;
|
|
27
27
|
items: GitHubSearchItem[];
|
|
28
28
|
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch issues from maintained repos using REST API (no Search API quota).
|
|
31
|
+
*
|
|
32
|
+
* Checks each repo for recent push activity and star threshold,
|
|
33
|
+
* then fetches open issues via `GET /repos/{owner}/{repo}/issues`.
|
|
34
|
+
* Falls back to the caller to use Search API if this doesn't yield enough.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fetchIssuesFromMaintainedRepos(octokit: Octokit, repos: string[], minStars: number, maxResults: number): Promise<GitHubSearchItem[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch open issues from known repos using REST API (no Search API quota).
|
|
39
|
+
* Used by Phase 0 (merged-PR repos) and Phase 1 (starred repos).
|
|
40
|
+
*
|
|
41
|
+
* Instead of the Search API (`octokit.search.issuesAndPullRequests`), this
|
|
42
|
+
* calls `GET /repos/{owner}/{repo}/issues` which counts against the much
|
|
43
|
+
* larger Core API rate limit and avoids consuming the scarce Search quota.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fetchIssuesFromKnownRepos(octokit: Octokit, vetter: IssueVetter, repos: string[], labels: string[], maxResults: number, priority: SearchPriority, filterFn: (items: GitHubSearchItem[]) => GitHubSearchItem[]): Promise<{
|
|
46
|
+
candidates: IssueCandidate[];
|
|
47
|
+
allReposFailed: boolean;
|
|
48
|
+
rateLimitHit: boolean;
|
|
49
|
+
}>;
|
|
29
50
|
/**
|
|
30
51
|
* Search across chunked labels with deduplication.
|
|
31
52
|
*
|