@oss-autopilot/core 0.54.0 → 0.56.0
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.
- package/dist/cli.bundle.cjs +63 -63
- package/dist/commands/comments.js +0 -1
- package/dist/commands/config.js +45 -5
- package/dist/commands/daily.js +190 -157
- package/dist/commands/dashboard-data.js +37 -30
- package/dist/commands/dashboard-server.js +0 -1
- package/dist/commands/dismiss.js +0 -6
- package/dist/commands/init.js +0 -1
- package/dist/commands/local-repos.js +1 -2
- package/dist/commands/move.js +12 -11
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.js +166 -130
- package/dist/commands/shelve.js +10 -10
- package/dist/commands/startup.js +30 -14
- package/dist/core/ci-analysis.d.ts +6 -0
- package/dist/core/ci-analysis.js +89 -12
- package/dist/core/daily-logic.js +24 -33
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/issue-discovery.d.ts +7 -44
- package/dist/core/issue-discovery.js +83 -188
- package/dist/core/issue-eligibility.d.ts +35 -0
- package/dist/core/issue-eligibility.js +126 -0
- package/dist/core/issue-vetting.d.ts +6 -21
- package/dist/core/issue-vetting.js +15 -279
- package/dist/core/pr-monitor.d.ts +7 -12
- package/dist/core/pr-monitor.js +14 -80
- package/dist/core/repo-health.d.ts +24 -0
- package/dist/core/repo-health.js +193 -0
- package/dist/core/search-phases.d.ts +55 -0
- package/dist/core/search-phases.js +155 -0
- package/dist/core/state.d.ts +11 -0
- package/dist/core/state.js +63 -4
- package/dist/core/types.d.ts +8 -1
- package/dist/core/types.js +7 -0
- package/dist/formatters/json.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
const MODULE = 'issue-eligibility';
|
|
12
|
+
/** Phrases that indicate someone has already claimed an issue. */
|
|
13
|
+
const CLAIM_PHRASES = [
|
|
14
|
+
"i'm working on this",
|
|
15
|
+
'i am working on this',
|
|
16
|
+
"i'll take this",
|
|
17
|
+
'i will take this',
|
|
18
|
+
'working on it',
|
|
19
|
+
"i'd like to work on",
|
|
20
|
+
'i would like to work on',
|
|
21
|
+
'can i work on',
|
|
22
|
+
'may i work on',
|
|
23
|
+
'assigned to me',
|
|
24
|
+
"i'm on it",
|
|
25
|
+
"i'll submit a pr",
|
|
26
|
+
'i will submit a pr',
|
|
27
|
+
'working on a fix',
|
|
28
|
+
'working on a pr',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Check whether an open PR already exists for the given issue.
|
|
32
|
+
* Searches both the PR search index and the issue timeline for linked PRs.
|
|
33
|
+
*/
|
|
34
|
+
export async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
35
|
+
try {
|
|
36
|
+
// Search for PRs that mention this issue
|
|
37
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
38
|
+
q: `repo:${owner}/${repo} is:pr ${issueNumber}`,
|
|
39
|
+
per_page: 5,
|
|
40
|
+
});
|
|
41
|
+
// Also check timeline for linked PRs
|
|
42
|
+
const timeline = await paginateAll((page) => octokit.issues.listEventsForTimeline({
|
|
43
|
+
owner,
|
|
44
|
+
repo,
|
|
45
|
+
issue_number: issueNumber,
|
|
46
|
+
per_page: 100,
|
|
47
|
+
page,
|
|
48
|
+
}));
|
|
49
|
+
const linkedPRs = timeline.filter((event) => {
|
|
50
|
+
const e = event;
|
|
51
|
+
return e.event === 'cross-referenced' && e.source?.issue?.pull_request;
|
|
52
|
+
});
|
|
53
|
+
return { passed: data.total_count === 0 && linkedPRs.length === 0 };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const errMsg = errorMessage(error);
|
|
57
|
+
warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
58
|
+
return { passed: true, inconclusive: true, reason: errMsg };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check how many merged PRs the authenticated user has in a repo.
|
|
63
|
+
* Uses GitHub Search API. Returns 0 on error (non-fatal).
|
|
64
|
+
*/
|
|
65
|
+
export async function checkUserMergedPRsInRepo(octokit, owner, repo) {
|
|
66
|
+
try {
|
|
67
|
+
// Use @me to search as the authenticated user
|
|
68
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
69
|
+
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
70
|
+
per_page: 1, // We only need total_count
|
|
71
|
+
});
|
|
72
|
+
return data.total_count;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const errMsg = errorMessage(error);
|
|
76
|
+
warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check whether an issue has been claimed by another contributor
|
|
82
|
+
* by scanning recent comments for claim phrases.
|
|
83
|
+
*/
|
|
84
|
+
export async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount) {
|
|
85
|
+
if (commentCount === 0)
|
|
86
|
+
return { passed: true };
|
|
87
|
+
try {
|
|
88
|
+
// Paginate through all comments
|
|
89
|
+
const comments = await octokit.paginate(octokit.issues.listComments, {
|
|
90
|
+
owner,
|
|
91
|
+
repo,
|
|
92
|
+
issue_number: issueNumber,
|
|
93
|
+
per_page: 100,
|
|
94
|
+
}, (response) => response.data);
|
|
95
|
+
// Limit to last 100 comments to avoid excessive processing
|
|
96
|
+
const recentComments = comments.slice(-100);
|
|
97
|
+
for (const comment of recentComments) {
|
|
98
|
+
const body = (comment.body || '').toLowerCase();
|
|
99
|
+
if (CLAIM_PHRASES.some((phrase) => body.includes(phrase))) {
|
|
100
|
+
return { passed: false };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { passed: true };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const errMsg = errorMessage(error);
|
|
107
|
+
warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
108
|
+
return { passed: true, inconclusive: true, reason: errMsg };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Analyze whether an issue body has clear, actionable requirements.
|
|
113
|
+
* Returns true when at least two "clarity indicators" are present:
|
|
114
|
+
* numbered/bulleted steps, code blocks, expected-behavior keywords, length > 200.
|
|
115
|
+
*/
|
|
116
|
+
export function analyzeRequirements(body) {
|
|
117
|
+
if (!body || body.length < 50)
|
|
118
|
+
return false;
|
|
119
|
+
// Check for clear structure
|
|
120
|
+
const hasSteps = /\d\.|[-*]\s/.test(body);
|
|
121
|
+
const hasCodeBlock = /```/.test(body);
|
|
122
|
+
const hasExpectedBehavior = /expect|should|must|want/i.test(body);
|
|
123
|
+
// Must have at least two indicators of clarity
|
|
124
|
+
const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
|
|
125
|
+
return indicators.filter(Boolean).length >= 2;
|
|
126
|
+
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Issue Vetting —
|
|
3
|
-
*
|
|
2
|
+
* Issue Vetting — orchestrates individual issue checks and computes
|
|
3
|
+
* recommendation + viability score.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Delegates to focused modules (#621):
|
|
6
|
+
* - issue-eligibility.ts — PR existence, claim detection, requirements analysis
|
|
7
|
+
* - repo-health.ts — project health, contribution guidelines
|
|
6
8
|
*/
|
|
7
9
|
import { Octokit } from '@octokit/rest';
|
|
8
|
-
import {
|
|
10
|
+
import { type SearchPriority, type IssueCandidate } from './types.js';
|
|
9
11
|
import { getStateManager } from './state.js';
|
|
10
|
-
/** Result of a vetting check that may be inconclusive due to API errors. */
|
|
11
|
-
export interface CheckResult {
|
|
12
|
-
passed: boolean;
|
|
13
|
-
inconclusive?: boolean;
|
|
14
|
-
reason?: string;
|
|
15
|
-
}
|
|
16
12
|
export declare class IssueVetter {
|
|
17
13
|
private octokit;
|
|
18
14
|
private stateManager;
|
|
@@ -29,17 +25,6 @@ export declare class IssueVetter {
|
|
|
29
25
|
allFailed: boolean;
|
|
30
26
|
rateLimitHit: boolean;
|
|
31
27
|
}>;
|
|
32
|
-
checkNoExistingPR(owner: string, repo: string, issueNumber: number): Promise<CheckResult>;
|
|
33
|
-
/**
|
|
34
|
-
* Check how many merged PRs the authenticated user has in a repo.
|
|
35
|
-
* Uses GitHub Search API. Returns 0 on error (non-fatal).
|
|
36
|
-
*/
|
|
37
|
-
checkUserMergedPRsInRepo(owner: string, repo: string): Promise<number>;
|
|
38
|
-
checkNotClaimed(owner: string, repo: string, issueNumber: number, commentCount: number): Promise<CheckResult>;
|
|
39
|
-
checkProjectHealth(owner: string, repo: string): Promise<ProjectHealth>;
|
|
40
|
-
fetchContributionGuidelines(owner: string, repo: string): Promise<ContributionGuidelines | undefined>;
|
|
41
|
-
parseContributionGuidelines(content: string): ContributionGuidelines;
|
|
42
|
-
analyzeRequirements(body: string): boolean;
|
|
43
28
|
/**
|
|
44
29
|
* Get the repo score from state, or return null if not evaluated
|
|
45
30
|
*/
|
|
@@ -1,41 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Issue Vetting —
|
|
3
|
-
*
|
|
2
|
+
* Issue Vetting — orchestrates individual issue checks and computes
|
|
3
|
+
* recommendation + viability score.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Delegates to focused modules (#621):
|
|
6
|
+
* - issue-eligibility.ts — PR existence, claim detection, requirements analysis
|
|
7
|
+
* - repo-health.ts — project health, contribution guidelines
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { parseGitHubUrl, daysBetween, DEFAULT_CONCURRENCY } from './utils.js';
|
|
9
|
+
import { parseGitHubUrl, DEFAULT_CONCURRENCY } from './utils.js';
|
|
9
10
|
import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
|
|
10
11
|
import { warn } from './logger.js';
|
|
11
|
-
import { getHttpCache, cachedRequest, cachedTimeBased } from './http-cache.js';
|
|
12
12
|
import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
|
|
13
13
|
import { repoBelongsToCategory } from './category-mapping.js';
|
|
14
|
+
import { checkNoExistingPR, checkNotClaimed, checkUserMergedPRsInRepo, analyzeRequirements, } from './issue-eligibility.js';
|
|
15
|
+
import { checkProjectHealth, fetchContributionGuidelines } from './repo-health.js';
|
|
14
16
|
const MODULE = 'issue-vetting';
|
|
15
17
|
const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
|
|
16
|
-
// Cache for contribution guidelines (expires after 1 hour, max 100 entries)
|
|
17
|
-
const guidelinesCache = new Map();
|
|
18
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
19
|
-
/** TTL for cached project health results (4 hours). Health data (stars, commits, CI) changes slowly. */
|
|
20
|
-
const HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1000;
|
|
21
|
-
const CACHE_MAX_SIZE = 100;
|
|
22
|
-
function pruneCache() {
|
|
23
|
-
const now = Date.now();
|
|
24
|
-
// First, remove expired entries (older than CACHE_TTL_MS)
|
|
25
|
-
for (const [key, value] of guidelinesCache.entries()) {
|
|
26
|
-
if (now - value.fetchedAt > CACHE_TTL_MS) {
|
|
27
|
-
guidelinesCache.delete(key);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Then, if still over size limit, remove oldest entries
|
|
31
|
-
if (guidelinesCache.size > CACHE_MAX_SIZE) {
|
|
32
|
-
const entries = Array.from(guidelinesCache.entries()).sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
|
|
33
|
-
const toRemove = entries.slice(0, guidelinesCache.size - CACHE_MAX_SIZE);
|
|
34
|
-
for (const [key] of toRemove) {
|
|
35
|
-
guidelinesCache.delete(key);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
18
|
export class IssueVetter {
|
|
40
19
|
octokit;
|
|
41
20
|
stateManager;
|
|
@@ -60,18 +39,18 @@ export class IssueVetter {
|
|
|
60
39
|
repo,
|
|
61
40
|
issue_number: number,
|
|
62
41
|
});
|
|
63
|
-
// Run all vetting checks in parallel
|
|
42
|
+
// Run all vetting checks in parallel — delegates to standalone functions
|
|
64
43
|
const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
44
|
+
checkNoExistingPR(this.octokit, owner, repo, number),
|
|
45
|
+
checkNotClaimed(this.octokit, owner, repo, number, ghIssue.comments),
|
|
46
|
+
checkProjectHealth(this.octokit, owner, repo),
|
|
47
|
+
fetchContributionGuidelines(this.octokit, owner, repo),
|
|
48
|
+
checkUserMergedPRsInRepo(this.octokit, owner, repo),
|
|
70
49
|
]);
|
|
71
50
|
const noExistingPR = existingPRCheck.passed;
|
|
72
51
|
const notClaimed = claimCheck.passed;
|
|
73
52
|
// Analyze issue quality
|
|
74
|
-
const clearRequirements =
|
|
53
|
+
const clearRequirements = analyzeRequirements(ghIssue.body || '');
|
|
75
54
|
// When the health check itself failed (API error), use a neutral default:
|
|
76
55
|
// don't penalize the repo as inactive, but don't credit it as active either.
|
|
77
56
|
const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
|
|
@@ -285,249 +264,6 @@ export class IssueVetter {
|
|
|
285
264
|
}
|
|
286
265
|
return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
|
|
287
266
|
}
|
|
288
|
-
async checkNoExistingPR(owner, repo, issueNumber) {
|
|
289
|
-
try {
|
|
290
|
-
// Search for PRs that mention this issue
|
|
291
|
-
const { data } = await this.octokit.search.issuesAndPullRequests({
|
|
292
|
-
q: `repo:${owner}/${repo} is:pr ${issueNumber}`,
|
|
293
|
-
per_page: 5,
|
|
294
|
-
});
|
|
295
|
-
// Also check timeline for linked PRs
|
|
296
|
-
const timeline = await paginateAll((page) => this.octokit.issues.listEventsForTimeline({
|
|
297
|
-
owner,
|
|
298
|
-
repo,
|
|
299
|
-
issue_number: issueNumber,
|
|
300
|
-
per_page: 100,
|
|
301
|
-
page,
|
|
302
|
-
}));
|
|
303
|
-
const linkedPRs = timeline.filter((event) => {
|
|
304
|
-
const e = event;
|
|
305
|
-
return e.event === 'cross-referenced' && e.source?.issue?.pull_request;
|
|
306
|
-
});
|
|
307
|
-
return { passed: data.total_count === 0 && linkedPRs.length === 0 };
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
const errMsg = errorMessage(error);
|
|
311
|
-
warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
312
|
-
return { passed: true, inconclusive: true, reason: errMsg };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Check how many merged PRs the authenticated user has in a repo.
|
|
317
|
-
* Uses GitHub Search API. Returns 0 on error (non-fatal).
|
|
318
|
-
*/
|
|
319
|
-
async checkUserMergedPRsInRepo(owner, repo) {
|
|
320
|
-
try {
|
|
321
|
-
// Use @me to search as the authenticated user
|
|
322
|
-
const { data } = await this.octokit.search.issuesAndPullRequests({
|
|
323
|
-
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
324
|
-
per_page: 1, // We only need total_count
|
|
325
|
-
});
|
|
326
|
-
return data.total_count;
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
const errMsg = errorMessage(error);
|
|
330
|
-
warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
331
|
-
return 0;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
async checkNotClaimed(owner, repo, issueNumber, commentCount) {
|
|
335
|
-
if (commentCount === 0)
|
|
336
|
-
return { passed: true };
|
|
337
|
-
try {
|
|
338
|
-
// Paginate through all comments (up to 100)
|
|
339
|
-
const comments = await this.octokit.paginate(this.octokit.issues.listComments, {
|
|
340
|
-
owner,
|
|
341
|
-
repo,
|
|
342
|
-
issue_number: issueNumber,
|
|
343
|
-
per_page: 100,
|
|
344
|
-
}, (response) => response.data);
|
|
345
|
-
// Limit to last 100 comments to avoid excessive processing
|
|
346
|
-
const recentComments = comments.slice(-100);
|
|
347
|
-
// Look for claiming phrases
|
|
348
|
-
const claimPhrases = [
|
|
349
|
-
"i'm working on this",
|
|
350
|
-
'i am working on this',
|
|
351
|
-
"i'll take this",
|
|
352
|
-
'i will take this',
|
|
353
|
-
'working on it',
|
|
354
|
-
"i'd like to work on",
|
|
355
|
-
'i would like to work on',
|
|
356
|
-
'can i work on',
|
|
357
|
-
'may i work on',
|
|
358
|
-
'assigned to me',
|
|
359
|
-
"i'm on it",
|
|
360
|
-
"i'll submit a pr",
|
|
361
|
-
'i will submit a pr',
|
|
362
|
-
'working on a fix',
|
|
363
|
-
'working on a pr',
|
|
364
|
-
];
|
|
365
|
-
for (const comment of recentComments) {
|
|
366
|
-
const body = (comment.body || '').toLowerCase();
|
|
367
|
-
if (claimPhrases.some((phrase) => body.includes(phrase))) {
|
|
368
|
-
return { passed: false };
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { passed: true };
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
const errMsg = errorMessage(error);
|
|
375
|
-
warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
376
|
-
return { passed: true, inconclusive: true, reason: errMsg };
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async checkProjectHealth(owner, repo) {
|
|
380
|
-
const cache = getHttpCache();
|
|
381
|
-
const healthCacheKey = `health:${owner}/${repo}`;
|
|
382
|
-
try {
|
|
383
|
-
return await cachedTimeBased(cache, healthCacheKey, HEALTH_CACHE_TTL_MS, async () => {
|
|
384
|
-
// Get repo info (with ETag caching — repo metadata changes infrequently)
|
|
385
|
-
const url = `/repos/${owner}/${repo}`;
|
|
386
|
-
const repoData = await cachedRequest(cache, url, (headers) => this.octokit.repos.get({ owner, repo, headers }));
|
|
387
|
-
// Get recent commits
|
|
388
|
-
const { data: commits } = await this.octokit.repos.listCommits({
|
|
389
|
-
owner,
|
|
390
|
-
repo,
|
|
391
|
-
per_page: 1,
|
|
392
|
-
});
|
|
393
|
-
const lastCommit = commits[0];
|
|
394
|
-
const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
|
|
395
|
-
const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
|
|
396
|
-
// Check CI status (simplified - just check if workflows exist)
|
|
397
|
-
let ciStatus = 'unknown';
|
|
398
|
-
try {
|
|
399
|
-
const { data: workflows } = await this.octokit.actions.listRepoWorkflows({
|
|
400
|
-
owner,
|
|
401
|
-
repo,
|
|
402
|
-
per_page: 1,
|
|
403
|
-
});
|
|
404
|
-
if (workflows.total_count > 0) {
|
|
405
|
-
ciStatus = 'passing'; // Assume passing if workflows exist
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
const errMsg = errorMessage(error);
|
|
410
|
-
warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${errMsg}. Defaulting to unknown.`);
|
|
411
|
-
}
|
|
412
|
-
return {
|
|
413
|
-
repo: `${owner}/${repo}`,
|
|
414
|
-
lastCommitAt,
|
|
415
|
-
daysSinceLastCommit,
|
|
416
|
-
openIssuesCount: repoData.open_issues_count,
|
|
417
|
-
avgIssueResponseDays: 0, // Would need more API calls to calculate
|
|
418
|
-
ciStatus,
|
|
419
|
-
isActive: daysSinceLastCommit < 30,
|
|
420
|
-
stargazersCount: repoData.stargazers_count,
|
|
421
|
-
forksCount: repoData.forks_count,
|
|
422
|
-
};
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
const errMsg = errorMessage(error);
|
|
427
|
-
warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
428
|
-
return {
|
|
429
|
-
repo: `${owner}/${repo}`,
|
|
430
|
-
lastCommitAt: '',
|
|
431
|
-
daysSinceLastCommit: 999,
|
|
432
|
-
openIssuesCount: 0,
|
|
433
|
-
avgIssueResponseDays: 0,
|
|
434
|
-
ciStatus: 'unknown',
|
|
435
|
-
isActive: false,
|
|
436
|
-
checkFailed: true,
|
|
437
|
-
failureReason: errMsg,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
async fetchContributionGuidelines(owner, repo) {
|
|
442
|
-
const cacheKey = `${owner}/${repo}`;
|
|
443
|
-
// Check cache first
|
|
444
|
-
const cached = guidelinesCache.get(cacheKey);
|
|
445
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
446
|
-
return cached.guidelines;
|
|
447
|
-
}
|
|
448
|
-
const filesToCheck = ['CONTRIBUTING.md', '.github/CONTRIBUTING.md', 'docs/CONTRIBUTING.md', 'contributing.md'];
|
|
449
|
-
// Probe all paths in parallel — take the first success in priority order
|
|
450
|
-
const results = await Promise.allSettled(filesToCheck.map((file) => this.octokit.repos.getContent({ owner, repo, path: file }).then(({ data }) => {
|
|
451
|
-
if ('content' in data) {
|
|
452
|
-
return Buffer.from(data.content, 'base64').toString('utf-8');
|
|
453
|
-
}
|
|
454
|
-
return null;
|
|
455
|
-
})));
|
|
456
|
-
for (let i = 0; i < results.length; i++) {
|
|
457
|
-
const result = results[i];
|
|
458
|
-
if (result.status === 'fulfilled' && result.value) {
|
|
459
|
-
const guidelines = this.parseContributionGuidelines(result.value);
|
|
460
|
-
guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
|
|
461
|
-
pruneCache();
|
|
462
|
-
return guidelines;
|
|
463
|
-
}
|
|
464
|
-
if (result.status === 'rejected') {
|
|
465
|
-
const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
466
|
-
if (!msg.includes('404') && !msg.includes('Not Found')) {
|
|
467
|
-
warn(MODULE, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${msg}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
// Cache the negative result too and prune if needed
|
|
472
|
-
guidelinesCache.set(cacheKey, { guidelines: undefined, fetchedAt: Date.now() });
|
|
473
|
-
pruneCache();
|
|
474
|
-
return undefined;
|
|
475
|
-
}
|
|
476
|
-
parseContributionGuidelines(content) {
|
|
477
|
-
const guidelines = {
|
|
478
|
-
rawContent: content,
|
|
479
|
-
};
|
|
480
|
-
const lowerContent = content.toLowerCase();
|
|
481
|
-
// Detect branch naming conventions
|
|
482
|
-
if (lowerContent.includes('branch')) {
|
|
483
|
-
const branchMatch = content.match(/branch[^\n]*(?:named?|format|convention)[^\n]*[`"]([^`"]+)[`"]/i);
|
|
484
|
-
if (branchMatch) {
|
|
485
|
-
guidelines.branchNamingConvention = branchMatch[1];
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
// Detect commit message format
|
|
489
|
-
if (lowerContent.includes('conventional commit')) {
|
|
490
|
-
guidelines.commitMessageFormat = 'conventional commits';
|
|
491
|
-
}
|
|
492
|
-
else if (lowerContent.includes('commit message')) {
|
|
493
|
-
const commitMatch = content.match(/commit message[^\n]*[`"]([^`"]+)[`"]/i);
|
|
494
|
-
if (commitMatch) {
|
|
495
|
-
guidelines.commitMessageFormat = commitMatch[1];
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
// Detect test framework
|
|
499
|
-
if (lowerContent.includes('jest'))
|
|
500
|
-
guidelines.testFramework = 'Jest';
|
|
501
|
-
else if (lowerContent.includes('rspec'))
|
|
502
|
-
guidelines.testFramework = 'RSpec';
|
|
503
|
-
else if (lowerContent.includes('pytest'))
|
|
504
|
-
guidelines.testFramework = 'pytest';
|
|
505
|
-
else if (lowerContent.includes('mocha'))
|
|
506
|
-
guidelines.testFramework = 'Mocha';
|
|
507
|
-
// Detect linter
|
|
508
|
-
if (lowerContent.includes('eslint'))
|
|
509
|
-
guidelines.linter = 'ESLint';
|
|
510
|
-
else if (lowerContent.includes('rubocop'))
|
|
511
|
-
guidelines.linter = 'RuboCop';
|
|
512
|
-
else if (lowerContent.includes('prettier'))
|
|
513
|
-
guidelines.formatter = 'Prettier';
|
|
514
|
-
// Detect CLA requirement
|
|
515
|
-
if (lowerContent.includes('cla') || lowerContent.includes('contributor license agreement')) {
|
|
516
|
-
guidelines.claRequired = true;
|
|
517
|
-
}
|
|
518
|
-
return guidelines;
|
|
519
|
-
}
|
|
520
|
-
analyzeRequirements(body) {
|
|
521
|
-
if (!body || body.length < 50)
|
|
522
|
-
return false;
|
|
523
|
-
// Check for clear structure
|
|
524
|
-
const hasSteps = /\d\.|[-*]\s/.test(body);
|
|
525
|
-
const hasCodeBlock = /```/.test(body);
|
|
526
|
-
const hasExpectedBehavior = /expect|should|must|want/i.test(body);
|
|
527
|
-
// Must have at least two indicators of clarity
|
|
528
|
-
const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
|
|
529
|
-
return indicators.filter(Boolean).length >= 2;
|
|
530
|
-
}
|
|
531
267
|
/**
|
|
532
268
|
* Get the repo score from state, or return null if not evaluated
|
|
533
269
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Score methods still write to state.
|
|
5
5
|
*
|
|
6
6
|
* Decomposed into focused modules (#263):
|
|
7
|
-
* - ci-analysis.ts: CI check classification and analysis
|
|
7
|
+
* - ci-analysis.ts: CI status fetching, check classification and analysis
|
|
8
8
|
* - review-analysis.ts: Review decision and comment detection
|
|
9
9
|
* - checklist-analysis.ts: PR body checklist analysis
|
|
10
10
|
* - maintainer-analysis.ts: Maintainer action hint extraction
|
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
import { FetchedPR, DailyDigest, ClosedPR, MergedPR, StarFilter } from './types.js';
|
|
16
16
|
import { type PRCountsResult } from './github-stats.js';
|
|
17
17
|
export { computeDisplayLabel } from './display-utils.js';
|
|
18
|
-
export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
|
|
18
|
+
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
19
19
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
20
20
|
export { determineStatus } from './status-determination.js';
|
|
21
|
+
/**
|
|
22
|
+
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
23
|
+
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasMergeConflict(mergeable: boolean | null, mergeableState: string | null): boolean;
|
|
21
26
|
export interface PRCheckFailure {
|
|
22
27
|
prUrl: string;
|
|
23
28
|
error: string;
|
|
@@ -44,16 +49,6 @@ export declare class PRMonitor {
|
|
|
44
49
|
* Centralizes PR construction and display label computation (#79).
|
|
45
50
|
*/
|
|
46
51
|
private buildFetchedPR;
|
|
47
|
-
/**
|
|
48
|
-
* Check if PR has merge conflict
|
|
49
|
-
*/
|
|
50
|
-
private hasMergeConflict;
|
|
51
|
-
/**
|
|
52
|
-
* Get CI status from combined status API and check runs.
|
|
53
|
-
* Returns status and names of failing checks for diagnostics.
|
|
54
|
-
* Delegates analysis to ci-analysis module.
|
|
55
|
-
*/
|
|
56
|
-
private getCIStatus;
|
|
57
52
|
/**
|
|
58
53
|
* Fetch merged PR counts and latest merge dates per repository for the configured user.
|
|
59
54
|
* Delegates to github-stats module.
|