Sign In

@oss-scout/core

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@oss-scout/core - npm Package Compare versions

Comparing version
1.4.1
to
1.5.0
+26
-1
dist/core/issue-vetting.d.ts

@@ -10,3 +10,3 @@ /**

import { Octokit } from "@octokit/rest";
import { type SearchPriority, type IssueCandidate, type ProjectCategory, type ScoutPreferences, type ScoutState, type MergedPRRecord, type ClosedPRRecord, type OpenPRRecord } from "./types.js";
import { type SearchPriority, type IssueCandidate, type ProjectCategory, type ProjectHealth, type ScoutPreferences, type ScoutState, type MergedPRRecord, type ClosedPRRecord, type OpenPRRecord } from "./types.js";
import { type PrefetchedIssueCore } from "./issue-graphql.js";

@@ -100,2 +100,17 @@ import { type SearchBudgetTracker } from "./search-budget.js";

/**
* Repo-intrinsic contribution-acceptance signal from the health snapshot
* (#248/#249/#1575). Does the repo merge PR-based contributions at all over the
* last 90 days?
* - `true` — merged at least one PR
* - `false` — had closed PRs but merged none (it rejects the work it receives)
* - `null` — no PR activity in the window, or health/counts unavailable;
* inconclusive → needs_review, never a hard skip
*
* This deliberately does NOT distinguish maintainer self-merges from outside
* contributions — a solo repo that merges only its own PRs still reads `true`.
* That contributor-diversity refinement is deferred (#248 secondary); the point
* here is only to stop auto-approving repos that merge nothing at all.
*/
export declare function repoAcceptsContributionsFromHealth(projectHealth: ProjectHealth): boolean | null;
/**
* Inputs to deriveRecommendation: the already-computed check results and

@@ -144,2 +159,12 @@ * affinity signals. Kept as a flat record of primitives so the derivation is a

passedAllChecks: boolean;
/**
* Whether the repo merges PR-based contributions, from repo-wide recent merge
* history (#248/#249/#1575). `true` = has recent merged PRs; `false` = had
* closed PRs but merged none, so "approve" is withheld even when the
* eligibility checks pass (the high-star zero-merge spam case); `null` =
* couldn't compute or no PR activity in the window, treated as inconclusive →
* needs_review, never a hard skip. Distinct from effectiveMergedCount, which
* is the VIEWER's own history. See repoAcceptsContributionsFromHealth.
*/
repoAcceptsContributions: boolean | null;
}

@@ -146,0 +171,0 @@ export interface RecommendationOutput {

@@ -27,2 +27,27 @@ /**

/**
* Repo-intrinsic contribution-acceptance signal from the health snapshot
* (#248/#249/#1575). Does the repo merge PR-based contributions at all over the
* last 90 days?
* - `true` — merged at least one PR
* - `false` — had closed PRs but merged none (it rejects the work it receives)
* - `null` — no PR activity in the window, or health/counts unavailable;
* inconclusive → needs_review, never a hard skip
*
* This deliberately does NOT distinguish maintainer self-merges from outside
* contributions — a solo repo that merges only its own PRs still reads `true`.
* That contributor-diversity refinement is deferred (#248 secondary); the point
* here is only to stop auto-approving repos that merge nothing at all.
*/
export function repoAcceptsContributionsFromHealth(projectHealth) {
if (projectHealth.checkFailed || projectHealth.recentMergedPRCount == null) {
return null;
}
if (projectHealth.recentMergedPRCount > 0)
return true;
// No merged PRs. A number (0) for the rate means there WERE closed PRs, none
// merged — real evidence the repo rejects contributions. A null rate means no
// closed PRs in the window at all (new/quiet repo) — too little to skip on.
return projectHealth.recentMergeRate == null ? null : false;
}
/**
* Derive the human-readable notes, approve/skip reasons, and the final

@@ -64,2 +89,6 @@ * recommendation from a vet's check results. Pure: no I/O, no state reads — the

notes.push("No CONTRIBUTING.md found");
if (input.repoAcceptsContributions === false)
notes.push("Repo has merged no PRs in the last 90 days");
else if (input.repoAcceptsContributions === null)
notes.push("Could not verify whether the repo merges PRs");
// Reasons to skip / approve.

@@ -82,2 +111,4 @@ if (!input.noExistingPR) {

reasonsToSkip.push("Unclear requirements");
if (input.repoAcceptsContributions === false)
reasonsToSkip.push("Repo has no recent merged PRs");
if (input.noExistingPR)

@@ -121,3 +152,7 @@ reasonsToApprove.push("No existing PR");

}
else if (input.passedAllChecks) {
else if (input.passedAllChecks &&
input.repoAcceptsContributions !== false) {
// Withhold "approve" from a repo that merges nobody, even when the
// eligibility checks pass — the high-star zero-merge spam case (#249/#1575).
// A `null` (couldn't compute) still reaches here and is downgraded below.
recommendation = "approve";

@@ -136,3 +171,4 @@ }

input.claimInconclusive ||
input.mergedCountInconclusive;
input.mergedCountInconclusive ||
input.repoAcceptsContributions === null;
if (recommendation === "approve" && hasInconclusiveChecks) {

@@ -203,3 +239,3 @@ recommendation = "needs_review";

checkNotClaimed(this.octokit, owner, repo, number, core.commentCount),
checkProjectHealth(this.octokit, owner, repo),
checkProjectHealth(this.octokit, owner, repo, this.budgetTracker),
fetchContributionGuidelines(this.octokit, owner, repo),

@@ -242,2 +278,7 @@ hasMergedPRsInRepo

: projectHealth.isActive;
// Repo-intrinsic merge-acceptance gate (#248/#249/#1575): does this repo
// merge PR-based contributions at all? A high-star repo that merges nothing
// is exactly the spam case that used to get auto-approved. Independent of
// our own contribution history, unlike effectiveMergedCount below.
const repoAcceptsContributions = repoAcceptsContributionsFromHealth(projectHealth);
const vettingResult = {

@@ -294,3 +335,4 @@ passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,

!!claimCheck.inconclusive ||
mergedCountInconclusive;
mergedCountInconclusive ||
repoAcceptsContributions === null;
const { notes, reasonsToApprove, reasonsToSkip, recommendation } = deriveRecommendation({

@@ -322,2 +364,3 @@ noExistingPR,

passedAllChecks: vettingResult.passedAllChecks,
repoAcceptsContributions,
});

@@ -324,0 +367,0 @@ vettingResult.notes = notes;

+2
-1

@@ -9,2 +9,3 @@ /**

import { type ContributionGuidelines, type ProjectHealth } from "./types.js";
import { type SearchBudgetTracker } from "./search-budget.js";
/**

@@ -14,3 +15,3 @@ * Check the health of a GitHub project: recent commits, CI status, star/fork counts.

*/
export declare function checkProjectHealth(octokit: Octokit, owner: string, repo: string): Promise<ProjectHealth>;
export declare function checkProjectHealth(octokit: Octokit, owner: string, repo: string, tracker?: SearchBudgetTracker): Promise<ProjectHealth>;
/**

@@ -17,0 +18,0 @@ * Fetch and parse CONTRIBUTING.md (or variants) from a GitHub repo.

@@ -12,3 +12,47 @@ /**

import { probeRepoFile } from "./probe-repo-file.js";
import { getSearchBudgetTracker, } from "./search-budget.js";
const MODULE = "repo-health";
/** Window for measuring repo-wide merge behavior. */
const MERGE_WINDOW_DAYS = 90;
/**
* Repo-intrinsic merge signal: how much outside work a repo actually merges,
* measured from repo-wide PR history over the last 90 days (#248/#249/#1575) —
* NOT the viewer's own contribution relationship. Two budget-tracked Search API
* calls (merged count, closed-unmerged count); the result is cached with the
* surrounding health snapshot, so repeated candidates from one repo pay nothing.
*
* Errors are NOT swallowed here: they propagate to checkProjectHealth's outer
* catch, which turns the whole snapshot into `checkFailed` (and, crucially, does
* not cache it) so a transient search blip self-heals on the next call. Handling
* the error here and returning nulls instead would pin an inconclusive signal in
* the 4h health cache for the whole repo. `recentMergedPRCount` is therefore
* always a real number on the success path; `recentMergeRate` is null only when
* there were genuinely no closed PRs in the window.
*/
async function fetchRepoMergeStats(octokit, owner, repo, tracker) {
const since = new Date(Date.now() - MERGE_WINDOW_DAYS * 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10);
const count = async (qualifiers) => {
await tracker.waitForBudget();
try {
const { data } = await octokit.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} ${qualifiers} closed:>=${since}`,
per_page: 1, // only total_count is needed
});
return data.total_count;
}
finally {
// Always record — a failed request still consumes rate-limit budget.
tracker.recordCall();
}
};
const merged = await count("is:pr is:merged");
const closedUnmerged = await count("is:pr is:unmerged is:closed");
const denom = merged + closedUnmerged;
return {
recentMergedPRCount: merged,
recentMergeRate: denom > 0 ? merged / denom : null,
};
}
// ── Cache for contribution guidelines ──

@@ -45,3 +89,6 @@ const guidelinesCache = new Map();

*/
export async function checkProjectHealth(octokit, owner, repo) {
export async function checkProjectHealth(octokit, owner, repo,
// Optional injected budget tracker for the repo-wide merge-stat searches.
// Defaults to the shared singleton so existing callers behave identically.
tracker = getSearchBudgetTracker()) {
const cache = getHttpCache();

@@ -67,2 +114,3 @@ const healthCacheKey = `health:${owner}/${repo}`;

const ciStatus = "unknown";
const mergeStats = await fetchRepoMergeStats(octokit, owner, repo, tracker);
return {

@@ -79,2 +127,4 @@ repo: `${owner}/${repo}`,

language: repoData.language,
recentMergedPRCount: mergeStats.recentMergedPRCount,
recentMergeRate: mergeStats.recentMergeRate,
};

@@ -81,0 +131,0 @@ });

@@ -19,2 +19,19 @@ /**

language?: string | null;
/**
* Repo-wide merged PRs in the last 90 days. Repo-intrinsic — independent of
* the viewer's own contribution history — so it distinguishes a repo that
* merges outside work from a high-star repo that merges nobody (#248, #249,
* #1575). `null` when the count could not be computed (API failure) or was
* not fetched; callers treat `null` as inconclusive, never a hard skip.
*/
recentMergedPRCount?: number | null;
/**
* Fraction of recently-closed PRs that were merged — merged / (merged +
* closed-unmerged) over the last 90 days. `null` when there were no closed
* PRs in the window or the counts could not be computed. Repo-intrinsic and
* part of the candidate output contract: consumed by the downstream success
* grade (oss-autopilot) so a healthy new repo grades on its own merits rather
* than the viewer's contribution relationship (#248). Not read within scout.
*/
recentMergeRate?: number | null;
/** Discriminant: a real snapshot is never `checkFailed`. */

@@ -21,0 +38,0 @@ checkFailed?: false;

{
"name": "@oss-scout/core",
"version": "1.4.1",
"version": "1.5.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