@oss-autopilot/core 0.54.0 → 0.55.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.
Files changed (37) hide show
  1. package/dist/cli.bundle.cjs +63 -63
  2. package/dist/commands/comments.js +0 -1
  3. package/dist/commands/config.js +45 -5
  4. package/dist/commands/daily.js +190 -157
  5. package/dist/commands/dashboard-data.js +37 -30
  6. package/dist/commands/dashboard-server.js +0 -1
  7. package/dist/commands/dismiss.js +0 -6
  8. package/dist/commands/init.js +0 -1
  9. package/dist/commands/local-repos.js +1 -2
  10. package/dist/commands/move.js +12 -11
  11. package/dist/commands/setup.d.ts +2 -1
  12. package/dist/commands/setup.js +166 -130
  13. package/dist/commands/shelve.js +10 -10
  14. package/dist/commands/startup.js +30 -14
  15. package/dist/core/ci-analysis.d.ts +6 -0
  16. package/dist/core/ci-analysis.js +89 -12
  17. package/dist/core/daily-logic.js +24 -33
  18. package/dist/core/index.d.ts +2 -1
  19. package/dist/core/index.js +2 -1
  20. package/dist/core/issue-discovery.d.ts +7 -44
  21. package/dist/core/issue-discovery.js +83 -188
  22. package/dist/core/issue-eligibility.d.ts +35 -0
  23. package/dist/core/issue-eligibility.js +126 -0
  24. package/dist/core/issue-vetting.d.ts +6 -21
  25. package/dist/core/issue-vetting.js +15 -279
  26. package/dist/core/pr-monitor.d.ts +7 -12
  27. package/dist/core/pr-monitor.js +14 -80
  28. package/dist/core/repo-health.d.ts +24 -0
  29. package/dist/core/repo-health.js +193 -0
  30. package/dist/core/search-phases.d.ts +55 -0
  31. package/dist/core/search-phases.js +155 -0
  32. package/dist/core/state.d.ts +11 -0
  33. package/dist/core/state.js +63 -4
  34. package/dist/core/types.d.ts +8 -1
  35. package/dist/core/types.js +7 -0
  36. package/dist/formatters/json.d.ts +1 -1
  37. 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 — checks individual issues for claimability, existing PRs,
3
- * project health, contribution guidelines, and requirement clarity.
2
+ * Issue Vetting — orchestrates individual issue checks and computes
3
+ * recommendation + viability score.
4
4
  *
5
- * Extracted from issue-discovery.ts (#356) to isolate vetting logic.
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 { ContributionGuidelines, ProjectHealth, type SearchPriority, type IssueCandidate } from './types.js';
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 — checks individual issues for claimability, existing PRs,
3
- * project health, contribution guidelines, and requirement clarity.
2
+ * Issue Vetting — orchestrates individual issue checks and computes
3
+ * recommendation + viability score.
4
4
  *
5
- * Extracted from issue-discovery.ts (#356) to isolate vetting logic.
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 { paginateAll } from './pagination.js';
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.checkNoExistingPR(owner, repo, number),
66
- this.checkNotClaimed(owner, repo, number, ghIssue.comments),
67
- this.checkProjectHealth(owner, repo),
68
- this.fetchContributionGuidelines(owner, repo),
69
- this.checkUserMergedPRsInRepo(owner, repo),
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 = this.analyzeRequirements(ghIssue.body || '');
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.