@oss-scout/core
Advanced tools
@@ -29,2 +29,3 @@ /** | ||
| console.log(` excludeOrgs: ${formatArray(prefs.excludeOrgs)}`); | ||
| console.log(` preferredOrgs: ${formatArray(prefs.preferredOrgs)}`); | ||
| console.log(` aiPolicyBlocklist: ${formatArray(prefs.aiPolicyBlocklist)}`); | ||
@@ -31,0 +32,0 @@ console.log(` defaultStrategy: ${prefs.defaultStrategy ? formatArray(prefs.defaultStrategy) : "(all)"}`); |
@@ -104,2 +104,44 @@ /** | ||
| } | ||
| /** | ||
| * Cap on org: qualifiers in the orgs-phase query. Each qualifier consumes one | ||
| * of GitHub Search's boolean operators, so an unbounded preferredOrgs list | ||
| * would starve (or disable) label filtering. Orgs beyond the cap are dropped | ||
| * with a warning. | ||
| */ | ||
| const MAX_QUERY_ORGS = 8; | ||
| /** Orgs phase: broad-style search scoped to the user's preferred orgs. */ | ||
| async function runPhaseOrgs(octokit, vetter, orgs, languages, isAnyLanguage, labels, maxResults, minStars, phase0RepoSet, starredRepoSet, filterIssues, tracker) { | ||
| info(MODULE, `Orgs phase: searching ${orgs.length} preferred org(s)...`); | ||
| const queryOrgs = orgs.slice(0, MAX_QUERY_ORGS); | ||
| if (orgs.length > MAX_QUERY_ORGS) { | ||
| warn(MODULE, `Orgs phase: capping query at ${MAX_QUERY_ORGS} org(s); dropping ${orgs | ||
| .slice(MAX_QUERY_ORGS) | ||
| .join(", ")}`); | ||
| } | ||
| // Multiple org: qualifiers OR together in the GitHub search syntax. | ||
| const orgQuery = queryOrgs.map((o) => `org:${o}`).join(" "); | ||
| try { | ||
| const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, (langQ) => `is:issue is:open ${langQ} no:assignee ${orgQuery}` | ||
| .replace(/ +/g, " ") | ||
| .trim(), maxResults * 3, tracker, 0, queryOrgs.length); | ||
| const { candidates, allVetFailed, rateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet], maxResults, minStars, "Orgs phase"); | ||
| info(MODULE, `Found ${candidates.length} candidates from preferred orgs`); | ||
| return { | ||
| candidates, | ||
| error: allVetFailed ? "all vetting failed" : null, | ||
| rateLimitHit, | ||
| }; | ||
| } | ||
| catch (error) { | ||
| if (getHttpStatusCode(error) === 401) | ||
| throw error; | ||
| const errMsg = errorMessage(error); | ||
| warn(MODULE, `Error in preferred-orgs search: ${errMsg}`); | ||
| return { | ||
| candidates: [], | ||
| error: errMsg, | ||
| rateLimitHit: isRateLimitError(error), | ||
| }; | ||
| } | ||
| } | ||
| /** Phase 1: Search starred repos. */ | ||
@@ -464,3 +506,6 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) { | ||
| // reservation would just shrink the result set with nothing to fill it. | ||
| const otherStrategiesCanRun = (starredRepos.length > 0 && enabledStrategies.has("starred")) || | ||
| const preferredOrgs = config.preferredOrgs ?? []; | ||
| const orgsPhaseEnabled = preferredOrgs.length > 0 && enabledStrategies.has("orgs"); | ||
| const otherStrategiesCanRun = orgsPhaseEnabled || | ||
| (starredRepos.length > 0 && enabledStrategies.has("starred")) || | ||
| enabledStrategies.has("broad") || | ||
@@ -481,2 +526,12 @@ enabledStrategies.has("maintained"); | ||
| } | ||
| // Orgs phase: preferred organizations (broad-style search with org: | ||
| // qualifiers). Runs before starred so org results get first claim on the | ||
| // remaining budget — the user asked for these orgs explicitly. | ||
| if (allCandidates.length < maxResults && orgsPhaseEnabled) { | ||
| await applyInterPhaseDelay(); | ||
| const remaining = maxResults - allCandidates.length; | ||
| const result = await runPhaseOrgs(this.octokit, this.vetter, preferredOrgs, languages, isAnyLanguage, labels, remaining, minStars, phase0RepoSet, starredRepoSet, filterIssues, tracker); | ||
| recordPhaseResult("orgs", result); | ||
| strategiesUsed.push("orgs"); | ||
| } | ||
| // Phase 1: Starred repos | ||
@@ -522,6 +577,10 @@ if (allCandidates.length < maxResults && | ||
| // "enough NEW work" — not "we re-found issues in the same repos" and not | ||
| // "we found issues the vetter already ruled out" (#265). | ||
| // "we found issues the vetter already ruled out" (#265). Preferred-org | ||
| // candidates are affinity results too — the user named those orgs — so | ||
| // they must not gate off the broad phase either. | ||
| const preferredOrgSet = new Set(preferredOrgs.map((o) => o.toLowerCase())); | ||
| const newRepoCandidateCount = allCandidates.filter((c) => c.recommendation !== "skip" && | ||
| !phase0RepoSet.has(c.issue.repo) && | ||
| !starredRepoSet.has(c.issue.repo)).length; | ||
| !starredRepoSet.has(c.issue.repo) && | ||
| !preferredOrgSet.has(c.issue.repo.split("/")[0]?.toLowerCase() ?? "")).length; | ||
| if (skipThreshold > 0 && newRepoCandidateCount >= skipThreshold) { | ||
@@ -580,2 +639,5 @@ info(MODULE, `Skipping broad search: already found ${newRepoCandidateCount} viable candidate(s) from new repos (threshold: ${skipThreshold})`); | ||
| : null, | ||
| phaseErrors["orgs"] | ||
| ? `Orgs phase (preferred orgs): ${phaseErrors["orgs"]}` | ||
| : null, | ||
| phaseErrors["1"] | ||
@@ -582,0 +644,0 @@ ? `Phase 1 (starred repos): ${phaseErrors["1"]}` |
@@ -20,2 +20,3 @@ /** | ||
| excludeOrgs: { type: "array" }, | ||
| preferredOrgs: { type: "array" }, | ||
| aiPolicyBlocklist: { type: "array" }, | ||
@@ -22,0 +23,0 @@ projectCategories: { |
@@ -27,2 +27,3 @@ /** | ||
| merged: "merged"; | ||
| orgs: "orgs"; | ||
| starred: "starred"; | ||
@@ -33,3 +34,3 @@ broad: "broad"; | ||
| /** All concrete strategies (excludes 'all' meta-strategy). */ | ||
| export declare const CONCRETE_STRATEGIES: readonly ["merged", "starred", "broad", "maintained"]; | ||
| export declare const CONCRETE_STRATEGIES: readonly ["merged", "orgs", "starred", "broad", "maintained"]; | ||
| export declare const RepoSignalsSchema: z.ZodObject<{ | ||
@@ -250,2 +251,3 @@ hasActiveMaintainers: z.ZodBoolean; | ||
| excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>; | ||
| preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>; | ||
| aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>; | ||
@@ -272,2 +274,3 @@ projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{ | ||
| merged: "merged"; | ||
| orgs: "orgs"; | ||
| starred: "starred"; | ||
@@ -334,2 +337,3 @@ broad: "broad"; | ||
| excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>; | ||
| preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>; | ||
| aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>; | ||
@@ -356,2 +360,3 @@ projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{ | ||
| merged: "merged"; | ||
| orgs: "orgs"; | ||
| starred: "starred"; | ||
@@ -358,0 +363,0 @@ broad: "broad"; |
@@ -30,2 +30,3 @@ /** | ||
| "merged", | ||
| "orgs", | ||
| "starred", | ||
@@ -39,2 +40,3 @@ "broad", | ||
| "merged", | ||
| "orgs", | ||
| "starred", | ||
@@ -180,2 +182,5 @@ "broad", | ||
| excludeOrgs: z.array(z.string()).default([]), | ||
| // Orgs whose repos the "orgs" search phase targets with org: qualifiers. | ||
| // Empty (the default) disables the phase even when the strategy is enabled. | ||
| preferredOrgs: z.array(z.string()).default([]), | ||
| aiPolicyBlocklist: z.array(z.string()).default(["matplotlib/matplotlib"]), | ||
@@ -182,0 +187,0 @@ projectCategories: z.array(ProjectCategorySchema).default([]), |
@@ -117,4 +117,6 @@ /** | ||
| * never needs clamping by the caller. | ||
| * @param reservedOps OR operators already consumed by repo/org filters in | ||
| * the base query, forwarded to label chunking | ||
| */ | ||
| export declare function searchAcrossLanguagesAndLabels(octokit: Octokit, languages: string[], isAnyLanguage: boolean, labels: string[], buildBaseQuery: (langQuery: string) => string, perPage: number, tracker?: SearchBudgetTracker, startOffset?: number): Promise<GitHubSearchItem[]>; | ||
| export declare function searchAcrossLanguagesAndLabels(octokit: Octokit, languages: string[], isAnyLanguage: boolean, labels: string[], buildBaseQuery: (langQuery: string) => string, perPage: number, tracker?: SearchBudgetTracker, startOffset?: number, reservedOps?: number): Promise<GitHubSearchItem[]>; | ||
| /** | ||
@@ -121,0 +123,0 @@ * Shared pipeline: spam-filter, repo-exclusion, vetting, and star-count filter. |
@@ -400,4 +400,6 @@ /** | ||
| * never needs clamping by the caller. | ||
| * @param reservedOps OR operators already consumed by repo/org filters in | ||
| * the base query, forwarded to label chunking | ||
| */ | ||
| export async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker(), startOffset = 0) { | ||
| export async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker(), startOffset = 0, reservedOps = 0) { | ||
| const variants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0); | ||
@@ -411,3 +413,3 @@ const k = variants.length > 0 ? startOffset % variants.length : 0; | ||
| await sleep(GRAPHQL_INTER_QUERY_DELAY_MS); | ||
| const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}` | ||
| const items = await searchWithChunkedLabels(octokit, labels, reservedOps, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}` | ||
| .replace(/ +/g, " ") | ||
@@ -414,0 +416,0 @@ .trim(), perPage, tracker); |
+1
-1
| { | ||
| "name": "@oss-scout/core", | ||
| "version": "1.5.2", | ||
| "version": "1.6.0", | ||
| "description": "Personalized GitHub issue finder with multi-strategy search, deep vetting, and viability scoring — CLI, library, MCP server, and Claude Code plugin", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1143649
0.49%14772
0.57%