@oss-scout/core 1.1.0 → 1.2.1
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.
|
@@ -28,6 +28,29 @@ const MODULE = "issue-discovery";
|
|
|
28
28
|
const LOW_BUDGET_THRESHOLD = 20;
|
|
29
29
|
/** If remaining search quota is below this, only run Phase 0. */
|
|
30
30
|
const CRITICAL_BUDGET_THRESHOLD = 10;
|
|
31
|
+
/**
|
|
32
|
+
* Page size for Phase 0 (repos the user has contributed to). Larger than the
|
|
33
|
+
* default 5 so the backlog of open issues in known repos is reachable, not
|
|
34
|
+
* just the 5 newest-created. One `listForRepo` call regardless of page size,
|
|
35
|
+
* so this widens the candidate pool at no extra REST cost.
|
|
36
|
+
*/
|
|
37
|
+
const PHASE0_PER_PAGE = 30;
|
|
38
|
+
/**
|
|
39
|
+
* Max issue age (by last activity) for Phase 0 contributed repos. Relaxed well
|
|
40
|
+
* past the default `maxIssueAgeDays` (90) because in a repo the user already
|
|
41
|
+
* knows, an older-but-still-open issue is still worth evaluating — the vetter
|
|
42
|
+
* screens staleness, existing PRs, and claims downstream.
|
|
43
|
+
*/
|
|
44
|
+
const CONTRIBUTED_REPO_MAX_AGE_DAYS = 365;
|
|
45
|
+
/**
|
|
46
|
+
* Cap on Phase 0's share of `maxResults`. Phase 0 (contributed repos) fetches
|
|
47
|
+
* deeply (`PHASE0_PER_PAGE`) and can otherwise fill the entire result budget,
|
|
48
|
+
* which makes the `allCandidates.length < maxResults` gate false for every
|
|
49
|
+
* later phase so starred (Phase 1) and broad (Phases 2/3) never run. Reserving
|
|
50
|
+
* half the budget for the other strategies keeps each search round varied
|
|
51
|
+
* instead of returning only contributed-repo results.
|
|
52
|
+
*/
|
|
53
|
+
const PHASE0_MAX_SHARE = 0.5;
|
|
31
54
|
/** Build a reusable filter function from config. */
|
|
32
55
|
function buildIssueFilter(config) {
|
|
33
56
|
return (items) => {
|
|
@@ -63,8 +86,8 @@ function buildIssueFilter(config) {
|
|
|
63
86
|
}
|
|
64
87
|
/** Phase 0: Search repos where user has merged PRs (highest merge probability). */
|
|
65
88
|
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
66
|
-
info(MODULE, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
67
|
-
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
89
|
+
info(MODULE, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter, ${PHASE0_PER_PAGE}/repo)...`);
|
|
90
|
+
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues, PHASE0_PER_PAGE);
|
|
68
91
|
info(MODULE, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
69
92
|
return {
|
|
70
93
|
candidates,
|
|
@@ -374,15 +397,24 @@ export class IssueDiscovery {
|
|
|
374
397
|
if (aiBlocklisted.size > 0) {
|
|
375
398
|
debug(MODULE, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
|
|
376
399
|
}
|
|
377
|
-
const
|
|
400
|
+
const baseFilterConfig = {
|
|
378
401
|
excludedRepos: new Set(config.excludeRepos.map((r) => r.toLowerCase())),
|
|
379
402
|
excludeOrgs: new Set((config.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
380
403
|
aiBlocklisted,
|
|
381
404
|
lowScoringRepos,
|
|
382
405
|
skippedUrls: options.skippedUrls ?? new Set(),
|
|
383
|
-
maxAgeDays: config.maxIssueAgeDays || 90,
|
|
384
406
|
now: new Date(),
|
|
385
407
|
includeDocIssues: config.includeDocIssues ?? true,
|
|
408
|
+
};
|
|
409
|
+
const filterIssues = buildIssueFilter({
|
|
410
|
+
...baseFilterConfig,
|
|
411
|
+
maxAgeDays: config.maxIssueAgeDays || 90,
|
|
412
|
+
});
|
|
413
|
+
// Phase 0 (contributed repos) gets a relaxed age window so the existing
|
|
414
|
+
// backlog surfaces, not just issues active in the last 90 days.
|
|
415
|
+
const filterIssuesPhase0 = buildIssueFilter({
|
|
416
|
+
...baseFilterConfig,
|
|
417
|
+
maxAgeDays: CONTRIBUTED_REPO_MAX_AGE_DAYS,
|
|
386
418
|
});
|
|
387
419
|
// Phase 0: Repos the user has engaged with — merged PRs first (strongest
|
|
388
420
|
// signal), then open PRs (active engagement even without a merge yet).
|
|
@@ -398,10 +430,21 @@ export class IssueDiscovery {
|
|
|
398
430
|
break;
|
|
399
431
|
}
|
|
400
432
|
const phase0RepoSet = new Set(phase0Repos);
|
|
433
|
+
// Only cap Phase 0 when a later phase can actually consume the reserved
|
|
434
|
+
// budget — otherwise (no starred repos, broad/maintained disabled) the
|
|
435
|
+
// reservation would just shrink the result set with nothing to fill it.
|
|
436
|
+
const otherStrategiesCanRun = (starredRepos.length > 0 && enabledStrategies.has("starred")) ||
|
|
437
|
+
enabledStrategies.has("broad") ||
|
|
438
|
+
enabledStrategies.has("maintained");
|
|
401
439
|
if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
|
|
402
|
-
|
|
440
|
+
// Cap Phase 0's share so it can't consume the whole budget and starve
|
|
441
|
+
// the starred/broad phases (which gate on allCandidates < maxResults).
|
|
442
|
+
const phase0Cap = otherStrategiesCanRun
|
|
443
|
+
? Math.max(1, Math.ceil(maxResults * PHASE0_MAX_SHARE))
|
|
444
|
+
: maxResults;
|
|
445
|
+
const remaining = Math.min(maxResults - allCandidates.length, phase0Cap);
|
|
403
446
|
if (remaining > 0) {
|
|
404
|
-
const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining,
|
|
447
|
+
const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssuesPhase0);
|
|
405
448
|
recordPhaseResult("0", result);
|
|
406
449
|
}
|
|
407
450
|
strategiesUsed.push("merged");
|
|
@@ -43,7 +43,7 @@ export declare function fetchIssuesFromMaintainedRepos(octokit: Octokit, repos:
|
|
|
43
43
|
* calls `GET /repos/{owner}/{repo}/issues` which counts against the much
|
|
44
44
|
* larger Core API rate limit and avoids consuming the scarce Search quota.
|
|
45
45
|
*/
|
|
46
|
-
export declare function fetchIssuesFromKnownRepos(octokit: Octokit, vetter: IssueVetter, repos: string[], labels: string[], maxResults: number, priority: SearchPriority, filterFn: (items: GitHubSearchItem[]) => GitHubSearchItem[]): Promise<{
|
|
46
|
+
export declare function fetchIssuesFromKnownRepos(octokit: Octokit, vetter: IssueVetter, repos: string[], labels: string[], maxResults: number, priority: SearchPriority, filterFn: (items: GitHubSearchItem[]) => GitHubSearchItem[], perPage?: number): Promise<{
|
|
47
47
|
candidates: IssueCandidate[];
|
|
48
48
|
allReposFailed: boolean;
|
|
49
49
|
rateLimitHit: boolean;
|
|
@@ -186,7 +186,7 @@ export async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, m
|
|
|
186
186
|
* calls `GET /repos/{owner}/{repo}/issues` which counts against the much
|
|
187
187
|
* larger Core API rate limit and avoids consuming the scarce Search quota.
|
|
188
188
|
*/
|
|
189
|
-
export async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn) {
|
|
189
|
+
export async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn, perPage = 5) {
|
|
190
190
|
const candidates = [];
|
|
191
191
|
let failedRepos = 0;
|
|
192
192
|
let rateLimitFailures = 0;
|
|
@@ -213,7 +213,7 @@ export async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels,
|
|
|
213
213
|
state: "open",
|
|
214
214
|
sort: "created",
|
|
215
215
|
direction: "desc",
|
|
216
|
-
per_page:
|
|
216
|
+
per_page: perPage,
|
|
217
217
|
...(label !== undefined ? { labels: label } : {}),
|
|
218
218
|
});
|
|
219
219
|
for (const issue of response.data) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/core",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Personalized GitHub issue finder with multi-strategy search, deep vetting, and viability scoring — CLI, library, MCP server, and Claude Code plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|