@oss-scout/core 0.9.0 → 0.9.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.
|
@@ -21,7 +21,7 @@ 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, fetchIssuesFromMaintainedRepos, filterVetAndScore, fetchIssuesFromKnownRepos,
|
|
24
|
+
import { buildEffectiveLabels, interleaveArrays, cachedSearchIssues, fetchIssuesFromMaintainedRepos, filterVetAndScore, fetchIssuesFromKnownRepos, searchAcrossLanguagesAndLabels, } from "./search-phases.js";
|
|
25
25
|
const MODULE = "issue-discovery";
|
|
26
26
|
/** If remaining search quota is below this, skip heavy phases (2, 3). */
|
|
27
27
|
const LOW_BUDGET_THRESHOLD = 20;
|
|
@@ -83,7 +83,7 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
/** Phase 2: General label-filtered search with multi-tier interleaving. */
|
|
86
|
-
async function runPhase2(octokit, vetter, scopes, labels, configLabels,
|
|
86
|
+
async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
87
87
|
info(MODULE, "Phase 2: General issue search...");
|
|
88
88
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
89
89
|
// Build per-tier label groups. Multi-tier when 2+ scopes; single-tier otherwise.
|
|
@@ -112,7 +112,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
112
112
|
let rateLimitHit = false;
|
|
113
113
|
for (const { tier, tierLabels } of tierLabelGroups) {
|
|
114
114
|
try {
|
|
115
|
-
const allItems = await
|
|
115
|
+
const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
|
|
116
116
|
info(MODULE, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
|
|
117
117
|
const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit, } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
|
|
118
118
|
tierResults.push(tierCandidates);
|
|
@@ -337,9 +337,6 @@ export class IssueDiscovery {
|
|
|
337
337
|
const langQuery = isAnyLanguage
|
|
338
338
|
? ""
|
|
339
339
|
: languages.map((l) => `language:${l}`).join(" ");
|
|
340
|
-
const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`
|
|
341
|
-
.replace(/ +/g, " ")
|
|
342
|
-
.trim();
|
|
343
340
|
// Build reusable filter
|
|
344
341
|
const aiBlocklisted = new Set(config.aiPolicyBlocklist);
|
|
345
342
|
if (aiBlocklisted.size > 0) {
|
|
@@ -427,7 +424,7 @@ export class IssueDiscovery {
|
|
|
427
424
|
info(MODULE, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
428
425
|
}
|
|
429
426
|
const remaining = maxResults - allCandidates.length;
|
|
430
|
-
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config.labels,
|
|
427
|
+
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
431
428
|
allCandidates.push(...result.candidates);
|
|
432
429
|
phaseErrors["2"] = result.error;
|
|
433
430
|
if (result.rateLimitHit)
|
|
@@ -61,6 +61,30 @@ export declare function fetchIssuesFromKnownRepos(octokit: Octokit, vetter: Issu
|
|
|
61
61
|
* @param perPage Number of results per API call
|
|
62
62
|
*/
|
|
63
63
|
export declare function searchWithChunkedLabels(octokit: Octokit, labels: string[], reservedOps: number, buildQuery: (labelQuery: string) => string, perPage: number): Promise<GitHubSearchItem[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Build per-call language qualifier strings, fanning out across languages
|
|
66
|
+
* when a multi-language + labels combination would trip GitHub Search's
|
|
67
|
+
* empty-result edge case (multi-`language:` AND with a label OR-group
|
|
68
|
+
* silently returns 0 — see https://github.com/costajohnt/oss-autopilot/issues/1331).
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildLanguageVariants(languages: string[], isAnyLanguage: boolean, hasLabels: boolean): string[];
|
|
71
|
+
/**
|
|
72
|
+
* Search across languages with label chunking, deduplicating results.
|
|
73
|
+
*
|
|
74
|
+
* Fans out one query per language when 2+ languages are paired with labels
|
|
75
|
+
* (works around a GitHub Search backend edge case where the multi-language
|
|
76
|
+
* AND combined with a label OR-group returns 0). For each language variant,
|
|
77
|
+
* delegates to searchWithChunkedLabels to keep within GitHub's 5-operator limit.
|
|
78
|
+
*
|
|
79
|
+
* @param octokit Authenticated Octokit instance
|
|
80
|
+
* @param languages Configured languages (used as `language:X` qualifiers)
|
|
81
|
+
* @param isAnyLanguage When true, skip language qualifiers entirely
|
|
82
|
+
* @param labels Label list passed to searchWithChunkedLabels
|
|
83
|
+
* @param buildBaseQuery Builds the query prefix from a language qualifier string;
|
|
84
|
+
* e.g. `(langQ) => `is:issue is:open ${langQ} no:assignee`.trim()`
|
|
85
|
+
* @param perPage Results per API call
|
|
86
|
+
*/
|
|
87
|
+
export declare function searchAcrossLanguagesAndLabels(octokit: Octokit, languages: string[], isAnyLanguage: boolean, labels: string[], buildBaseQuery: (langQuery: string) => string, perPage: number): Promise<GitHubSearchItem[]>;
|
|
64
88
|
/**
|
|
65
89
|
* Shared pipeline: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
66
90
|
* Used by Phases 2 and 3 to convert raw search results into vetted candidates.
|
|
@@ -291,6 +291,56 @@ export async function searchWithChunkedLabels(octokit, labels, reservedOps, buil
|
|
|
291
291
|
}
|
|
292
292
|
return allItems;
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Build per-call language qualifier strings, fanning out across languages
|
|
296
|
+
* when a multi-language + labels combination would trip GitHub Search's
|
|
297
|
+
* empty-result edge case (multi-`language:` AND with a label OR-group
|
|
298
|
+
* silently returns 0 — see https://github.com/costajohnt/oss-autopilot/issues/1331).
|
|
299
|
+
*/
|
|
300
|
+
export function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
|
|
301
|
+
if (isAnyLanguage || languages.length === 0)
|
|
302
|
+
return [""];
|
|
303
|
+
if (languages.length === 1)
|
|
304
|
+
return [`language:${languages[0]}`];
|
|
305
|
+
if (!hasLabels)
|
|
306
|
+
return [languages.map((l) => `language:${l}`).join(" ")];
|
|
307
|
+
return languages.map((l) => `language:${l}`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Search across languages with label chunking, deduplicating results.
|
|
311
|
+
*
|
|
312
|
+
* Fans out one query per language when 2+ languages are paired with labels
|
|
313
|
+
* (works around a GitHub Search backend edge case where the multi-language
|
|
314
|
+
* AND combined with a label OR-group returns 0). For each language variant,
|
|
315
|
+
* delegates to searchWithChunkedLabels to keep within GitHub's 5-operator limit.
|
|
316
|
+
*
|
|
317
|
+
* @param octokit Authenticated Octokit instance
|
|
318
|
+
* @param languages Configured languages (used as `language:X` qualifiers)
|
|
319
|
+
* @param isAnyLanguage When true, skip language qualifiers entirely
|
|
320
|
+
* @param labels Label list passed to searchWithChunkedLabels
|
|
321
|
+
* @param buildBaseQuery Builds the query prefix from a language qualifier string;
|
|
322
|
+
* e.g. `(langQ) => `is:issue is:open ${langQ} no:assignee`.trim()`
|
|
323
|
+
* @param perPage Results per API call
|
|
324
|
+
*/
|
|
325
|
+
export async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
|
|
326
|
+
const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
|
|
327
|
+
const seenUrls = new Set();
|
|
328
|
+
const allItems = [];
|
|
329
|
+
for (let i = 0; i < langVariants.length; i++) {
|
|
330
|
+
if (i > 0)
|
|
331
|
+
await sleep(INTER_QUERY_DELAY_MS);
|
|
332
|
+
const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`
|
|
333
|
+
.replace(/ +/g, " ")
|
|
334
|
+
.trim(), perPage);
|
|
335
|
+
for (const item of items) {
|
|
336
|
+
if (!seenUrls.has(item.html_url)) {
|
|
337
|
+
seenUrls.add(item.html_url);
|
|
338
|
+
allItems.push(item);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return allItems;
|
|
343
|
+
}
|
|
294
344
|
/**
|
|
295
345
|
* Shared pipeline: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
296
346
|
* Used by Phases 2 and 3 to convert raw search results into vetted candidates.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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": {
|