@oss-autopilot/core 1.11.0 → 1.12.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.
@@ -1,151 +0,0 @@
1
- /**
2
- * Issue Eligibility — checks whether an individual issue is claimable:
3
- * existing PR detection, claim-phrase scanning, user merge history,
4
- * and requirement clarity analysis.
5
- *
6
- * Extracted from issue-vetting.ts (#621) to isolate eligibility logic.
7
- */
8
- import { paginateAll } from './pagination.js';
9
- import { errorMessage } from './errors.js';
10
- import { warn } from './logger.js';
11
- import { getHttpCache } from './http-cache.js';
12
- import { getSearchBudgetTracker } from './search-budget.js';
13
- const MODULE = 'issue-eligibility';
14
- /** Phrases that indicate someone has already claimed an issue. */
15
- const CLAIM_PHRASES = [
16
- "i'm working on this",
17
- 'i am working on this',
18
- "i'll take this",
19
- 'i will take this',
20
- 'working on it',
21
- "i'd like to work on",
22
- 'i would like to work on',
23
- 'can i work on',
24
- 'may i work on',
25
- 'assigned to me',
26
- "i'm on it",
27
- "i'll submit a pr",
28
- 'i will submit a pr',
29
- 'working on a fix',
30
- 'working on a pr',
31
- ];
32
- /**
33
- * Check whether an open PR already exists for the given issue.
34
- * Uses the timeline API (REST) to detect cross-referenced PRs, avoiding
35
- * the Search API's strict 30 req/min rate limit.
36
- */
37
- export async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
38
- try {
39
- // Use the timeline API (REST, not Search) to detect linked PRs.
40
- // This avoids consuming GitHub Search API quota (30 req/min limit).
41
- // Timeline captures formally linked PRs via cross-referenced events
42
- // but may miss PRs that only mention the issue number without a formal
43
- // link — an acceptable trade-off since most PRs use "Fixes #N" syntax.
44
- const timeline = await paginateAll((page) => octokit.issues.listEventsForTimeline({
45
- owner,
46
- repo,
47
- issue_number: issueNumber,
48
- per_page: 100,
49
- page,
50
- }));
51
- const linkedPRs = timeline.filter((event) => {
52
- const e = event;
53
- return e.event === 'cross-referenced' && e.source?.issue?.pull_request;
54
- });
55
- return { passed: linkedPRs.length === 0 };
56
- }
57
- catch (error) {
58
- const errMsg = errorMessage(error);
59
- warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
60
- return { passed: true, inconclusive: true, reason: errMsg };
61
- }
62
- }
63
- /** TTL for cached merged-PR counts per repo (15 minutes). */
64
- const MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1000;
65
- /**
66
- * Check how many merged PRs the authenticated user has in a repo.
67
- * Uses GitHub Search API. Returns 0 on error (non-fatal).
68
- * Results are cached per-repo for 15 minutes to avoid redundant Search API
69
- * calls when multiple issues from the same repo are vetted.
70
- */
71
- export async function checkUserMergedPRsInRepo(octokit, owner, repo) {
72
- const cache = getHttpCache();
73
- const cacheKey = `merged-prs:${owner}/${repo}`;
74
- // Manual cache check — do not use cachedTimeBased because we must NOT cache
75
- // error-path fallback values (a transient failure returning 0 would poison the
76
- // cache for 15 minutes, hiding that the user has merged PRs in the repo).
77
- const cached = cache.getIfFresh(cacheKey, MERGED_PR_CACHE_TTL_MS);
78
- if (cached != null && typeof cached === 'number') {
79
- return cached;
80
- }
81
- try {
82
- const tracker = getSearchBudgetTracker();
83
- await tracker.waitForBudget();
84
- try {
85
- // Use @me to search as the authenticated user
86
- const { data } = await octokit.search.issuesAndPullRequests({
87
- q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
88
- per_page: 1, // We only need total_count
89
- });
90
- // Only cache successful results
91
- cache.set(cacheKey, '', data.total_count);
92
- return data.total_count;
93
- }
94
- finally {
95
- // Always record the call — failed requests still consume GitHub rate limit points
96
- tracker.recordCall();
97
- }
98
- }
99
- catch (error) {
100
- const errMsg = errorMessage(error);
101
- warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
102
- return 0; // Not cached — next call will retry
103
- }
104
- }
105
- /**
106
- * Check whether an issue has been claimed by another contributor
107
- * by scanning recent comments for claim phrases.
108
- */
109
- export async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount) {
110
- if (commentCount === 0)
111
- return { passed: true };
112
- try {
113
- // Paginate through all comments
114
- const comments = await octokit.paginate(octokit.issues.listComments, {
115
- owner,
116
- repo,
117
- issue_number: issueNumber,
118
- per_page: 100,
119
- }, (response) => response.data);
120
- // Limit to last 100 comments to avoid excessive processing
121
- const recentComments = comments.slice(-100);
122
- for (const comment of recentComments) {
123
- const body = (comment.body || '').toLowerCase();
124
- if (CLAIM_PHRASES.some((phrase) => body.includes(phrase))) {
125
- return { passed: false };
126
- }
127
- }
128
- return { passed: true };
129
- }
130
- catch (error) {
131
- const errMsg = errorMessage(error);
132
- warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
133
- return { passed: true, inconclusive: true, reason: errMsg };
134
- }
135
- }
136
- /**
137
- * Analyze whether an issue body has clear, actionable requirements.
138
- * Returns true when at least two "clarity indicators" are present:
139
- * numbered/bulleted steps, code blocks, expected-behavior keywords, length > 200.
140
- */
141
- export function analyzeRequirements(body) {
142
- if (!body || body.length < 50)
143
- return false;
144
- // Check for clear structure
145
- const hasSteps = /\d\.|[-*]\s/.test(body);
146
- const hasCodeBlock = /```/.test(body);
147
- const hasExpectedBehavior = /expect|should|must|want/i.test(body);
148
- // Must have at least two indicators of clarity
149
- const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
150
- return indicators.filter(Boolean).length >= 2;
151
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Issue Filtering — pure functions for filtering and spam detection on search results.
3
- *
4
- * Extracted from issue-discovery.ts (#356) to isolate filtering logic:
5
- * label farming detection, doc-only filtering, per-repo caps, templated title detection.
6
- */
7
- /** Minimal shape of a GitHub search result item (from octokit.search.issuesAndPullRequests) */
8
- export interface GitHubSearchItem {
9
- html_url: string;
10
- repository_url: string;
11
- updated_at: string;
12
- title?: string;
13
- labels?: Array<{
14
- name?: string;
15
- } | string>;
16
- [key: string]: unknown;
17
- }
18
- /** Labels that indicate documentation-only issues (#105). */
19
- export declare const DOC_ONLY_LABELS: Set<string>;
20
- /**
21
- * Check if an issue's labels are ALL documentation-related (#105).
22
- * Issues with mixed labels (e.g., "good first issue" + "documentation") pass through.
23
- * Issues with no labels are not considered doc-only.
24
- */
25
- export declare function isDocOnlyIssue(item: GitHubSearchItem): boolean;
26
- /** Known beginner-type label names used to detect label-farming repos (#97). */
27
- export declare const BEGINNER_LABELS: Set<string>;
28
- /** Check if a single issue has an excessive number of beginner labels (>= 5). */
29
- export declare function isLabelFarming(item: GitHubSearchItem): boolean;
30
- /** Detect mass-created issue titles like "Add Trivia Question 61" or "Create Entry #5". */
31
- export declare function hasTemplatedTitle(title: string): boolean;
32
- /**
33
- * Batch-analyze search items to detect label-farming repositories (#97).
34
- * Returns a Set of repo full names (owner/repo) that appear to be spam.
35
- *
36
- * A repo is flagged if:
37
- * - ANY single issue has >= 5 beginner labels (strong individual signal), OR
38
- * - It has >= 3 issues with templated titles (batch signal)
39
- */
40
- export declare function detectLabelFarmingRepos(items: GitHubSearchItem[]): Set<string>;
41
- /**
42
- * Apply per-repo cap to candidates (#105).
43
- * Keeps at most `maxPerRepo` issues from any single repo.
44
- * Maintains the existing sort order — first N from each repo are kept,
45
- * excess issues from over-represented repos are dropped.
46
- */
47
- export declare function applyPerRepoCap<T extends {
48
- issue: {
49
- repo: string;
50
- };
51
- }>(candidates: T[], maxPerRepo: number): T[];
@@ -1,103 +0,0 @@
1
- /**
2
- * Issue Filtering — pure functions for filtering and spam detection on search results.
3
- *
4
- * Extracted from issue-discovery.ts (#356) to isolate filtering logic:
5
- * label farming detection, doc-only filtering, per-repo caps, templated title detection.
6
- */
7
- /** Labels that indicate documentation-only issues (#105). */
8
- export const DOC_ONLY_LABELS = new Set(['documentation', 'docs', 'typo', 'spelling']);
9
- /**
10
- * Check if an issue's labels are ALL documentation-related (#105).
11
- * Issues with mixed labels (e.g., "good first issue" + "documentation") pass through.
12
- * Issues with no labels are not considered doc-only.
13
- */
14
- export function isDocOnlyIssue(item) {
15
- if (!item.labels || !Array.isArray(item.labels) || item.labels.length === 0)
16
- return false;
17
- const labelNames = item.labels.map((l) => (typeof l === 'string' ? l : l.name || '').toLowerCase());
18
- // Filter out empty label names before checking
19
- const nonEmptyLabels = labelNames.filter((n) => n.length > 0);
20
- if (nonEmptyLabels.length === 0)
21
- return false;
22
- return nonEmptyLabels.every((n) => DOC_ONLY_LABELS.has(n));
23
- }
24
- /** Known beginner-type label names used to detect label-farming repos (#97). */
25
- export const BEGINNER_LABELS = new Set([
26
- 'good first issue',
27
- 'hacktoberfest',
28
- 'easy',
29
- 'up-for-grabs',
30
- 'first-timers-only',
31
- 'beginner-friendly',
32
- 'beginner',
33
- 'starter',
34
- 'newbie',
35
- 'low-hanging-fruit',
36
- 'community',
37
- ]);
38
- /** Check if a single issue has an excessive number of beginner labels (>= 5). */
39
- export function isLabelFarming(item) {
40
- if (!item.labels || !Array.isArray(item.labels))
41
- return false;
42
- const labelNames = item.labels.map((l) => (typeof l === 'string' ? l : l.name || '').toLowerCase());
43
- const beginnerCount = labelNames.filter((n) => BEGINNER_LABELS.has(n)).length;
44
- return beginnerCount >= 5;
45
- }
46
- /** Detect mass-created issue titles like "Add Trivia Question 61" or "Create Entry #5". */
47
- export function hasTemplatedTitle(title) {
48
- if (!title)
49
- return false;
50
- // Matches "<anything> <category-noun> <number>" where category nouns are typical
51
- // of mass-created templated issues. This avoids false positives on legitimate titles
52
- // like "Add support for Python 3" or "Implement RFC 7231" which lack category nouns.
53
- return /^.+\s+(question|fact|point|item|task|entry|post|challenge|exercise|example|problem|tip|recipe|snippet)\s+#?\d+$/i.test(title);
54
- }
55
- /**
56
- * Batch-analyze search items to detect label-farming repositories (#97).
57
- * Returns a Set of repo full names (owner/repo) that appear to be spam.
58
- *
59
- * A repo is flagged if:
60
- * - ANY single issue has >= 5 beginner labels (strong individual signal), OR
61
- * - It has >= 3 issues with templated titles (batch signal)
62
- */
63
- export function detectLabelFarmingRepos(items) {
64
- const spamRepos = new Set();
65
- const repoSpamCounts = new Map();
66
- for (const item of items) {
67
- const repoFullName = item.repository_url.split('/').slice(-2).join('/');
68
- // Strong signal: single issue with 5+ beginner labels
69
- if (isLabelFarming(item)) {
70
- spamRepos.add(repoFullName);
71
- continue;
72
- }
73
- // Weaker signal: templated title
74
- if (item.title && hasTemplatedTitle(item.title)) {
75
- repoSpamCounts.set(repoFullName, (repoSpamCounts.get(repoFullName) || 0) + 1);
76
- }
77
- }
78
- // Flag repos with 3+ templated-title issues
79
- for (const [repo, count] of repoSpamCounts) {
80
- if (count >= 3) {
81
- spamRepos.add(repo);
82
- }
83
- }
84
- return spamRepos;
85
- }
86
- /**
87
- * Apply per-repo cap to candidates (#105).
88
- * Keeps at most `maxPerRepo` issues from any single repo.
89
- * Maintains the existing sort order — first N from each repo are kept,
90
- * excess issues from over-represented repos are dropped.
91
- */
92
- export function applyPerRepoCap(candidates, maxPerRepo) {
93
- const repoCounts = new Map();
94
- const kept = [];
95
- for (const c of candidates) {
96
- const count = repoCounts.get(c.issue.repo) || 0;
97
- if (count < maxPerRepo) {
98
- kept.push(c);
99
- repoCounts.set(c.issue.repo, count + 1);
100
- }
101
- }
102
- return kept;
103
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Issue Scoring — pure functions for computing viability scores and quality bonuses.
3
- *
4
- * Extracted from issue-discovery.ts (#356) to isolate scoring logic.
5
- */
6
- /**
7
- * Calculate a quality bonus based on repo star and fork counts (#98).
8
- * Stars: <50 → 0, 50-499 → +3, 500-4999 → +5, 5000+ → +8
9
- * Forks: 50+ → +2, 500+ → +4
10
- * Natural max is 12 (8 stars + 4 forks).
11
- */
12
- export declare function calculateRepoQualityBonus(stargazersCount: number, forksCount: number): number;
13
- export interface ViabilityScoreParams {
14
- repoScore: number | null;
15
- hasExistingPR: boolean;
16
- isClaimed: boolean;
17
- clearRequirements: boolean;
18
- hasContributionGuidelines: boolean;
19
- issueUpdatedAt: string;
20
- closedWithoutMergeCount: number;
21
- mergedPRCount: number;
22
- orgHasMergedPRs: boolean;
23
- repoQualityBonus?: number;
24
- /** True when the repo matches one of the user's preferred project categories. */
25
- matchesPreferredCategory?: boolean;
26
- }
27
- /**
28
- * Calculate viability score for an issue (0-100 scale)
29
- * Scoring:
30
- * - Base: 50 points
31
- * - +repoScore*2 (up to +20 for score of 10)
32
- * - +repoQualityBonus (up to +12 for established repos, from star/fork counts) (#98)
33
- * - +15 for merged PR in this repo (direct proven relationship) (#99)
34
- * - +15 for clear requirements (clarity)
35
- * - +15 for freshness (recently updated)
36
- * - +10 for contribution guidelines
37
- * - +5 for org affinity (merged PRs in same org)
38
- * - +5 for category preference (matches user's project categories)
39
- * - -30 if existing PR
40
- * - -20 if claimed
41
- * - -15 if closed-without-merge history with no merges
42
- */
43
- export declare function calculateViabilityScore(params: ViabilityScoreParams): number;
@@ -1,97 +0,0 @@
1
- /**
2
- * Issue Scoring — pure functions for computing viability scores and quality bonuses.
3
- *
4
- * Extracted from issue-discovery.ts (#356) to isolate scoring logic.
5
- */
6
- import { daysBetween } from './utils.js';
7
- /**
8
- * Calculate a quality bonus based on repo star and fork counts (#98).
9
- * Stars: <50 → 0, 50-499 → +3, 500-4999 → +5, 5000+ → +8
10
- * Forks: 50+ → +2, 500+ → +4
11
- * Natural max is 12 (8 stars + 4 forks).
12
- */
13
- export function calculateRepoQualityBonus(stargazersCount, forksCount) {
14
- let bonus = 0;
15
- // Star tiers
16
- if (stargazersCount >= 5000)
17
- bonus += 8;
18
- else if (stargazersCount >= 500)
19
- bonus += 5;
20
- else if (stargazersCount >= 50)
21
- bonus += 3;
22
- // Fork tiers
23
- if (forksCount >= 500)
24
- bonus += 4;
25
- else if (forksCount >= 50)
26
- bonus += 2;
27
- return bonus;
28
- }
29
- /**
30
- * Calculate viability score for an issue (0-100 scale)
31
- * Scoring:
32
- * - Base: 50 points
33
- * - +repoScore*2 (up to +20 for score of 10)
34
- * - +repoQualityBonus (up to +12 for established repos, from star/fork counts) (#98)
35
- * - +15 for merged PR in this repo (direct proven relationship) (#99)
36
- * - +15 for clear requirements (clarity)
37
- * - +15 for freshness (recently updated)
38
- * - +10 for contribution guidelines
39
- * - +5 for org affinity (merged PRs in same org)
40
- * - +5 for category preference (matches user's project categories)
41
- * - -30 if existing PR
42
- * - -20 if claimed
43
- * - -15 if closed-without-merge history with no merges
44
- */
45
- export function calculateViabilityScore(params) {
46
- let score = 50; // Base score
47
- // Add repo score contribution (up to +20)
48
- if (params.repoScore !== null) {
49
- score += params.repoScore * 2;
50
- }
51
- // Repo quality bonus from star/fork counts (#98, up to +12)
52
- score += params.repoQualityBonus ?? 0;
53
- // Merged PR bonus (+15) — direct proven relationship with this repo (#99)
54
- if (params.mergedPRCount > 0) {
55
- score += 15;
56
- }
57
- // Clarity bonus (+15)
58
- if (params.clearRequirements) {
59
- score += 15;
60
- }
61
- // Freshness bonus (+15 for issues updated within last 14 days)
62
- const updatedAt = new Date(params.issueUpdatedAt);
63
- const daysSinceUpdate = daysBetween(updatedAt);
64
- if (daysSinceUpdate <= 14) {
65
- score += 15;
66
- }
67
- else if (daysSinceUpdate <= 30) {
68
- // Partial bonus for 15-30 days
69
- score += Math.round(15 * (1 - (daysSinceUpdate - 14) / 16));
70
- }
71
- // Contribution guidelines bonus (+10)
72
- if (params.hasContributionGuidelines) {
73
- score += 10;
74
- }
75
- // Org affinity bonus (+5) — user has merged PRs in another repo under same org
76
- if (params.orgHasMergedPRs) {
77
- score += 5;
78
- }
79
- // Category preference bonus (+5) — repo matches user's preferred project categories
80
- if (params.matchesPreferredCategory) {
81
- score += 5;
82
- }
83
- // Penalty for existing PR (-30)
84
- if (params.hasExistingPR) {
85
- score -= 30;
86
- }
87
- // Penalty for claimed issue (-20)
88
- if (params.isClaimed) {
89
- score -= 20;
90
- }
91
- // Penalty for closed-without-merge history with no successful merges (-15)
92
- if (params.closedWithoutMergeCount > 0 && params.mergedPRCount === 0) {
93
- score -= 15;
94
- }
95
- // Clamp to 0-100
96
- return Math.max(0, Math.min(100, score));
97
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Issue Vetting — orchestrates individual issue checks and computes
3
- * recommendation + viability score.
4
- *
5
- * Delegates to focused modules (#621):
6
- * - issue-eligibility.ts — PR existence, claim detection, requirements analysis
7
- * - repo-health.ts — project health, contribution guidelines
8
- */
9
- import { Octokit } from '@octokit/rest';
10
- import { type SearchPriority, type IssueCandidate } from './types.js';
11
- import { getStateManager } from './state.js';
12
- export declare class IssueVetter {
13
- private octokit;
14
- private stateManager;
15
- constructor(octokit: Octokit, stateManager: ReturnType<typeof getStateManager>);
16
- /**
17
- * Vet a specific issue — runs all checks and computes recommendation + viability score.
18
- * Results are cached for 15 minutes to avoid redundant API calls on repeated searches.
19
- */
20
- vetIssue(issueUrl: string): Promise<IssueCandidate>;
21
- /**
22
- * Vet multiple issues in parallel with concurrency limit
23
- */
24
- vetIssuesParallel(urls: string[], maxResults: number, priority?: SearchPriority): Promise<{
25
- candidates: IssueCandidate[];
26
- allFailed: boolean;
27
- rateLimitHit: boolean;
28
- }>;
29
- /**
30
- * Get the repo score from state, or return null if not evaluated
31
- */
32
- private getRepoScore;
33
- }