@oss-scout/core 0.2.0 → 0.2.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.
Files changed (54) hide show
  1. package/dist/cli.bundle.cjs +42 -42
  2. package/dist/cli.js +110 -86
  3. package/dist/commands/config.d.ts +1 -1
  4. package/dist/commands/config.js +76 -72
  5. package/dist/commands/results.d.ts +1 -1
  6. package/dist/commands/results.js +1 -1
  7. package/dist/commands/search.d.ts +2 -2
  8. package/dist/commands/search.js +16 -6
  9. package/dist/commands/setup.d.ts +1 -1
  10. package/dist/commands/setup.js +27 -21
  11. package/dist/commands/validation.d.ts +1 -1
  12. package/dist/commands/validation.js +1 -1
  13. package/dist/commands/vet-list.d.ts +2 -2
  14. package/dist/commands/vet-list.js +12 -5
  15. package/dist/commands/vet.d.ts +3 -3
  16. package/dist/commands/vet.js +9 -5
  17. package/dist/core/bootstrap.d.ts +1 -1
  18. package/dist/core/bootstrap.js +20 -16
  19. package/dist/core/category-mapping.d.ts +1 -1
  20. package/dist/core/category-mapping.js +104 -13
  21. package/dist/core/errors.d.ts +8 -1
  22. package/dist/core/errors.js +31 -19
  23. package/dist/core/gist-state-store.d.ts +1 -1
  24. package/dist/core/gist-state-store.js +36 -27
  25. package/dist/core/github.d.ts +1 -1
  26. package/dist/core/github.js +5 -5
  27. package/dist/core/http-cache.js +26 -22
  28. package/dist/core/issue-discovery.d.ts +3 -3
  29. package/dist/core/issue-discovery.js +325 -277
  30. package/dist/core/issue-eligibility.d.ts +2 -2
  31. package/dist/core/issue-eligibility.js +26 -21
  32. package/dist/core/issue-filtering.js +23 -15
  33. package/dist/core/issue-scoring.js +1 -1
  34. package/dist/core/issue-vetting.d.ts +2 -2
  35. package/dist/core/issue-vetting.js +66 -53
  36. package/dist/core/local-state.d.ts +1 -1
  37. package/dist/core/local-state.js +16 -14
  38. package/dist/core/repo-health.d.ts +2 -2
  39. package/dist/core/repo-health.js +46 -35
  40. package/dist/core/schemas.d.ts +1 -1
  41. package/dist/core/schemas.js +40 -18
  42. package/dist/core/search-budget.js +3 -3
  43. package/dist/core/search-phases.d.ts +6 -6
  44. package/dist/core/search-phases.js +23 -19
  45. package/dist/core/types.d.ts +9 -9
  46. package/dist/core/types.js +15 -3
  47. package/dist/core/utils.d.ts +10 -1
  48. package/dist/core/utils.js +44 -25
  49. package/dist/formatters/json.d.ts +1 -1
  50. package/dist/index.d.ts +7 -7
  51. package/dist/index.js +5 -5
  52. package/dist/scout.d.ts +4 -5
  53. package/dist/scout.js +72 -31
  54. package/package.json +1 -1
@@ -5,8 +5,8 @@
5
5
  *
6
6
  * Extracted from issue-vetting.ts to isolate eligibility logic.
7
7
  */
8
- import { Octokit } from '@octokit/rest';
9
- import type { CheckResult } from './types.js';
8
+ import { Octokit } from "@octokit/rest";
9
+ import type { CheckResult } from "./types.js";
10
10
  /**
11
11
  * Check whether an open PR already exists for the given issue.
12
12
  * Uses the timeline API (REST) to detect cross-referenced PRs, avoiding
@@ -5,29 +5,29 @@
5
5
  *
6
6
  * Extracted from issue-vetting.ts to isolate eligibility logic.
7
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';
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
14
  /** Phrases that indicate someone has already claimed an issue. */
15
15
  const CLAIM_PHRASES = [
16
16
  "i'm working on this",
17
- 'i am working on this',
17
+ "i am working on this",
18
18
  "i'll take this",
19
- 'i will take this',
20
- 'working on it',
19
+ "i will take this",
20
+ "working on it",
21
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',
22
+ "i would like to work on",
23
+ "can i work on",
24
+ "may i work on",
25
+ "assigned to me",
26
26
  "i'm on it",
27
27
  "i'll submit a pr",
28
- 'i will submit a pr',
29
- 'working on a fix',
30
- 'working on a pr',
28
+ "i will submit a pr",
29
+ "working on a fix",
30
+ "working on a pr",
31
31
  ];
32
32
  /**
33
33
  * Check whether an open PR already exists for the given issue.
@@ -50,7 +50,7 @@ export async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
50
50
  }));
51
51
  const linkedPRs = timeline.filter((event) => {
52
52
  const e = event;
53
- return e.event === 'cross-referenced' && e.source?.issue?.pull_request;
53
+ return e.event === "cross-referenced" && e.source?.issue?.pull_request;
54
54
  });
55
55
  return { passed: linkedPRs.length === 0 };
56
56
  }
@@ -75,7 +75,7 @@ export async function checkUserMergedPRsInRepo(octokit, owner, repo) {
75
75
  // error-path fallback values (a transient failure returning 0 would poison the
76
76
  // cache for 15 minutes, hiding that the user has merged PRs in the repo).
77
77
  const cached = cache.getIfFresh(cacheKey, MERGED_PR_CACHE_TTL_MS);
78
- if (cached != null && typeof cached === 'number') {
78
+ if (cached != null && typeof cached === "number") {
79
79
  return cached;
80
80
  }
81
81
  try {
@@ -88,7 +88,7 @@ export async function checkUserMergedPRsInRepo(octokit, owner, repo) {
88
88
  per_page: 1, // We only need total_count
89
89
  });
90
90
  // Only cache successful results
91
- cache.set(cacheKey, '', data.total_count);
91
+ cache.set(cacheKey, "", data.total_count);
92
92
  return data.total_count;
93
93
  }
94
94
  finally {
@@ -120,7 +120,7 @@ export async function checkNotClaimed(octokit, owner, repo, issueNumber, comment
120
120
  // Limit to last 100 comments to avoid excessive processing
121
121
  const recentComments = comments.slice(-100);
122
122
  for (const comment of recentComments) {
123
- const body = (comment.body || '').toLowerCase();
123
+ const body = (comment.body || "").toLowerCase();
124
124
  if (CLAIM_PHRASES.some((phrase) => body.includes(phrase))) {
125
125
  return { passed: false };
126
126
  }
@@ -146,6 +146,11 @@ export function analyzeRequirements(body) {
146
146
  const hasCodeBlock = /```/.test(body);
147
147
  const hasExpectedBehavior = /expect|should|must|want/i.test(body);
148
148
  // Must have at least two indicators of clarity
149
- const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
149
+ const indicators = [
150
+ hasSteps,
151
+ hasCodeBlock,
152
+ hasExpectedBehavior,
153
+ body.length > 200,
154
+ ];
150
155
  return indicators.filter(Boolean).length >= 2;
151
156
  }
@@ -4,8 +4,14 @@
4
4
  * Extracted from issue-discovery.ts to isolate filtering logic:
5
5
  * label farming detection, doc-only filtering, per-repo caps, templated title detection.
6
6
  */
7
+ import { extractRepoFromUrl } from "./utils.js";
7
8
  /** Labels that indicate documentation-only issues. */
8
- export const DOC_ONLY_LABELS = new Set(['documentation', 'docs', 'typo', 'spelling']);
9
+ export const DOC_ONLY_LABELS = new Set([
10
+ "documentation",
11
+ "docs",
12
+ "typo",
13
+ "spelling",
14
+ ]);
9
15
  /**
10
16
  * Check if an issue's labels are ALL documentation-related.
11
17
  * Issues with mixed labels (e.g., "good first issue" + "documentation") pass through.
@@ -14,7 +20,7 @@ export const DOC_ONLY_LABELS = new Set(['documentation', 'docs', 'typo', 'spelli
14
20
  export function isDocOnlyIssue(item) {
15
21
  if (!item.labels || !Array.isArray(item.labels) || item.labels.length === 0)
16
22
  return false;
17
- const labelNames = item.labels.map((l) => (typeof l === 'string' ? l : l.name || '').toLowerCase());
23
+ const labelNames = item.labels.map((l) => (typeof l === "string" ? l : l.name || "").toLowerCase());
18
24
  // Filter out empty label names before checking
19
25
  const nonEmptyLabels = labelNames.filter((n) => n.length > 0);
20
26
  if (nonEmptyLabels.length === 0)
@@ -23,23 +29,23 @@ export function isDocOnlyIssue(item) {
23
29
  }
24
30
  /** Known beginner-type label names used to detect label-farming repos. */
25
31
  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',
32
+ "good first issue",
33
+ "hacktoberfest",
34
+ "easy",
35
+ "up-for-grabs",
36
+ "first-timers-only",
37
+ "beginner-friendly",
38
+ "beginner",
39
+ "starter",
40
+ "newbie",
41
+ "low-hanging-fruit",
42
+ "community",
37
43
  ]);
38
44
  /** Check if a single issue has an excessive number of beginner labels (>= 5). */
39
45
  export function isLabelFarming(item) {
40
46
  if (!item.labels || !Array.isArray(item.labels))
41
47
  return false;
42
- const labelNames = item.labels.map((l) => (typeof l === 'string' ? l : l.name || '').toLowerCase());
48
+ const labelNames = item.labels.map((l) => (typeof l === "string" ? l : l.name || "").toLowerCase());
43
49
  const beginnerCount = labelNames.filter((n) => BEGINNER_LABELS.has(n)).length;
44
50
  return beginnerCount >= 5;
45
51
  }
@@ -64,7 +70,9 @@ export function detectLabelFarmingRepos(items) {
64
70
  const spamRepos = new Set();
65
71
  const repoSpamCounts = new Map();
66
72
  for (const item of items) {
67
- const repoFullName = item.repository_url.split('/').slice(-2).join('/');
73
+ const repoFullName = extractRepoFromUrl(item.repository_url);
74
+ if (!repoFullName)
75
+ continue;
68
76
  // Strong signal: single issue with 5+ beginner labels
69
77
  if (isLabelFarming(item)) {
70
78
  spamRepos.add(repoFullName);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Extracted from issue-discovery.ts to isolate scoring logic.
5
5
  */
6
- import { daysBetween } from './utils.js';
6
+ import { daysBetween } from "./utils.js";
7
7
  /**
8
8
  * Calculate a quality bonus based on repo star and fork counts.
9
9
  * Stars: <50 -> 0, 50-499 -> +3, 500-4999 -> +5, 5000+ -> +8
@@ -6,8 +6,8 @@
6
6
  * - issue-eligibility.ts — PR existence, claim detection, requirements analysis
7
7
  * - repo-health.ts — project health, contribution guidelines
8
8
  */
9
- import { Octokit } from '@octokit/rest';
10
- import { type SearchPriority, type IssueCandidate, type ProjectCategory } from './types.js';
9
+ import { Octokit } from "@octokit/rest";
10
+ import { type SearchPriority, type IssueCandidate, type ProjectCategory } from "./types.js";
11
11
  /**
12
12
  * Read-only interface for accessing scout state during issue vetting.
13
13
  * Implementations may be backed by gist persistence, in-memory state, etc.
@@ -6,15 +6,15 @@
6
6
  * - issue-eligibility.ts — PR existence, claim detection, requirements analysis
7
7
  * - repo-health.ts — project health, contribution guidelines
8
8
  */
9
- import { parseGitHubUrl } from './utils.js';
10
- import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
11
- import { debug, warn } from './logger.js';
12
- import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
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';
16
- import { getHttpCache } from './http-cache.js';
17
- const MODULE = 'issue-vetting';
9
+ import { parseGitHubUrl } from "./utils.js";
10
+ import { ValidationError, errorMessage, isRateLimitError } from "./errors.js";
11
+ import { debug, warn } from "./logger.js";
12
+ import { calculateRepoQualityBonus, calculateViabilityScore, } from "./issue-scoring.js";
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";
16
+ import { getHttpCache } from "./http-cache.js";
17
+ const MODULE = "issue-vetting";
18
18
  /** Vetting concurrency: kept low to reduce burst pressure on GitHub's secondary rate limit. */
19
19
  const MAX_CONCURRENT_VETTING = 3;
20
20
  /** TTL for cached vetting results (15 minutes). Kept short so config changes take effect quickly. */
@@ -35,13 +35,16 @@ export class IssueVetter {
35
35
  const cache = getHttpCache();
36
36
  const cacheKey = `vet:${issueUrl}`;
37
37
  const cached = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
38
- if (cached && typeof cached === 'object' && 'issue' in cached && 'viabilityScore' in cached) {
38
+ if (cached &&
39
+ typeof cached === "object" &&
40
+ "issue" in cached &&
41
+ "viabilityScore" in cached) {
39
42
  debug(MODULE, `Vetting cache hit for ${issueUrl}`);
40
43
  return cached;
41
44
  }
42
45
  // Parse URL
43
46
  const parsed = parseGitHubUrl(issueUrl);
44
- if (!parsed || parsed.type !== 'issues') {
47
+ if (!parsed || parsed.type !== "issues") {
45
48
  throw new ValidationError(`Invalid issue URL: ${issueUrl}`);
46
49
  }
47
50
  const { owner, repo, number } = parsed;
@@ -56,20 +59,24 @@ export class IssueVetter {
56
59
  const reposWithMergedPRs = this.stateReader.getReposWithMergedPRs();
57
60
  const hasMergedPRsInRepo = reposWithMergedPRs.includes(repoFullName);
58
61
  // Run all vetting checks in parallel — delegates to standalone functions
59
- const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
62
+ const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount,] = await Promise.all([
60
63
  checkNoExistingPR(this.octokit, owner, repo, number),
61
64
  checkNotClaimed(this.octokit, owner, repo, number, ghIssue.comments),
62
65
  checkProjectHealth(this.octokit, owner, repo),
63
66
  fetchContributionGuidelines(this.octokit, owner, repo),
64
- hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo),
67
+ hasMergedPRsInRepo
68
+ ? Promise.resolve(0)
69
+ : checkUserMergedPRsInRepo(this.octokit, owner, repo),
65
70
  ]);
66
71
  const noExistingPR = existingPRCheck.passed;
67
72
  const notClaimed = claimCheck.passed;
68
73
  // Analyze issue quality
69
- const clearRequirements = analyzeRequirements(ghIssue.body || '');
74
+ const clearRequirements = analyzeRequirements(ghIssue.body || "");
70
75
  // When the health check itself failed (API error), use a neutral default:
71
76
  // don't penalize the repo as inactive, but don't credit it as active either.
72
- const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
77
+ const projectActive = projectHealth.checkFailed
78
+ ? true
79
+ : projectHealth.isActive;
73
80
  const vettingResult = {
74
81
  passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,
75
82
  checks: {
@@ -84,25 +91,25 @@ export class IssueVetter {
84
91
  };
85
92
  // Build notes
86
93
  if (!noExistingPR)
87
- vettingResult.notes.push('Existing PR found for this issue');
94
+ vettingResult.notes.push("Existing PR found for this issue");
88
95
  if (!notClaimed)
89
- vettingResult.notes.push('Issue appears to be claimed by someone');
96
+ vettingResult.notes.push("Issue appears to be claimed by someone");
90
97
  if (existingPRCheck.inconclusive) {
91
- vettingResult.notes.push(`Could not verify absence of existing PRs: ${existingPRCheck.reason || 'API error'}`);
98
+ vettingResult.notes.push(`Could not verify absence of existing PRs: ${existingPRCheck.reason || "API error"}`);
92
99
  }
93
100
  if (claimCheck.inconclusive) {
94
- vettingResult.notes.push(`Could not verify claim status: ${claimCheck.reason || 'API error'}`);
101
+ vettingResult.notes.push(`Could not verify claim status: ${claimCheck.reason || "API error"}`);
95
102
  }
96
103
  if (projectHealth.checkFailed) {
97
- vettingResult.notes.push(`Could not verify project activity: ${projectHealth.failureReason || 'API error'}`);
104
+ vettingResult.notes.push(`Could not verify project activity: ${projectHealth.failureReason || "API error"}`);
98
105
  }
99
106
  else if (!projectHealth.isActive) {
100
- vettingResult.notes.push('Project may be inactive');
107
+ vettingResult.notes.push("Project may be inactive");
101
108
  }
102
109
  if (!clearRequirements)
103
- vettingResult.notes.push('Issue requirements are unclear');
110
+ vettingResult.notes.push("Issue requirements are unclear");
104
111
  if (!contributionGuidelines)
105
- vettingResult.notes.push('No CONTRIBUTING.md found');
112
+ vettingResult.notes.push("No CONTRIBUTING.md found");
106
113
  // Create tracked issue
107
114
  const trackedIssue = {
108
115
  id: ghIssue.id,
@@ -110,8 +117,8 @@ export class IssueVetter {
110
117
  repo: repoFullName,
111
118
  number,
112
119
  title: ghIssue.title,
113
- status: 'candidate',
114
- labels: ghIssue.labels.map((l) => (typeof l === 'string' ? l : l.name || '')),
120
+ status: "candidate",
121
+ labels: ghIssue.labels.map((l) => typeof l === "string" ? l : l.name || ""),
115
122
  createdAt: ghIssue.created_at,
116
123
  updatedAt: ghIssue.updated_at,
117
124
  vetted: true,
@@ -121,34 +128,34 @@ export class IssueVetter {
121
128
  const reasonsToSkip = [];
122
129
  const reasonsToApprove = [];
123
130
  if (!noExistingPR)
124
- reasonsToSkip.push('Has existing PR');
131
+ reasonsToSkip.push("Has existing PR");
125
132
  if (!notClaimed)
126
- reasonsToSkip.push('Already claimed');
133
+ reasonsToSkip.push("Already claimed");
127
134
  if (!projectHealth.isActive && !projectHealth.checkFailed)
128
- reasonsToSkip.push('Inactive project');
135
+ reasonsToSkip.push("Inactive project");
129
136
  if (!clearRequirements)
130
- reasonsToSkip.push('Unclear requirements');
137
+ reasonsToSkip.push("Unclear requirements");
131
138
  if (noExistingPR)
132
- reasonsToApprove.push('No existing PR');
139
+ reasonsToApprove.push("No existing PR");
133
140
  if (notClaimed)
134
- reasonsToApprove.push('Not claimed');
141
+ reasonsToApprove.push("Not claimed");
135
142
  if (projectHealth.isActive && !projectHealth.checkFailed)
136
- reasonsToApprove.push('Active project');
143
+ reasonsToApprove.push("Active project");
137
144
  if (clearRequirements)
138
- reasonsToApprove.push('Clear requirements');
145
+ reasonsToApprove.push("Clear requirements");
139
146
  if (contributionGuidelines)
140
- reasonsToApprove.push('Has contribution guidelines');
147
+ reasonsToApprove.push("Has contribution guidelines");
141
148
  // Determine effective merged PR count: prefer local state (authoritative if present),
142
149
  // fall back to live GitHub API count to detect contributions made before using oss-scout
143
150
  const effectiveMergedCount = hasMergedPRsInRepo ? 1 : userMergedPRCount;
144
151
  if (effectiveMergedCount > 0) {
145
- reasonsToApprove.push(`Trusted project (${effectiveMergedCount} PR${effectiveMergedCount > 1 ? 's' : ''} merged)`);
152
+ reasonsToApprove.push(`Trusted project (${effectiveMergedCount} PR${effectiveMergedCount > 1 ? "s" : ""} merged)`);
146
153
  }
147
154
  // Check for org-level affinity (user has merged PRs in another repo under same org)
148
- const orgName = repoFullName.split('/')[0];
155
+ const orgName = repoFullName.split("/")[0];
149
156
  let orgHasMergedPRs = false;
150
- if (orgName && repoFullName.includes('/')) {
151
- orgHasMergedPRs = reposWithMergedPRs.some((r) => r.startsWith(orgName + '/') && r !== repoFullName);
157
+ if (orgName && repoFullName.includes("/")) {
158
+ orgHasMergedPRs = reposWithMergedPRs.some((r) => r.startsWith(orgName + "/") && r !== repoFullName);
152
159
  }
153
160
  if (orgHasMergedPRs) {
154
161
  reasonsToApprove.push(`Org affinity (merged PRs in other ${orgName} repos)`);
@@ -157,29 +164,31 @@ export class IssueVetter {
157
164
  const projectCategories = this.stateReader.getProjectCategories();
158
165
  const matchesCategory = repoBelongsToCategory(repoFullName, projectCategories);
159
166
  if (matchesCategory) {
160
- reasonsToApprove.push('Matches preferred project category');
167
+ reasonsToApprove.push("Matches preferred project category");
161
168
  }
162
169
  let recommendation;
163
170
  if (vettingResult.passedAllChecks) {
164
- recommendation = 'approve';
171
+ recommendation = "approve";
165
172
  }
166
173
  else if (reasonsToSkip.length > 2) {
167
- recommendation = 'skip';
174
+ recommendation = "skip";
168
175
  }
169
176
  else {
170
- recommendation = 'needs_review';
177
+ recommendation = "needs_review";
171
178
  }
172
179
  // Downgrade to needs_review if any check was inconclusive —
173
180
  // "approve" should only be given when all checks actually passed, not when they were skipped.
174
- const hasInconclusiveChecks = projectHealth.checkFailed || existingPRCheck.inconclusive || claimCheck.inconclusive;
175
- if (recommendation === 'approve' && hasInconclusiveChecks) {
176
- recommendation = 'needs_review';
177
- vettingResult.notes.push('Recommendation downgraded: one or more checks were inconclusive');
181
+ const hasInconclusiveChecks = projectHealth.checkFailed ||
182
+ existingPRCheck.inconclusive ||
183
+ claimCheck.inconclusive;
184
+ if (recommendation === "approve" && hasInconclusiveChecks) {
185
+ recommendation = "needs_review";
186
+ vettingResult.notes.push("Recommendation downgraded: one or more checks were inconclusive");
178
187
  }
179
188
  // Calculate repo quality bonus from star/fork counts
180
189
  const repoQualityBonus = calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
181
190
  if (projectHealth.checkFailed && repoQualityBonus === 0) {
182
- vettingResult.notes.push('Repo quality bonus unavailable: could not fetch star/fork counts due to API error');
191
+ vettingResult.notes.push("Repo quality bonus unavailable: could not fetch star/fork counts due to API error");
183
192
  }
184
193
  const repoScore = this.stateReader.getRepoScore(repoFullName);
185
194
  const viabilityScore = calculateViabilityScore({
@@ -197,15 +206,15 @@ export class IssueVetter {
197
206
  });
198
207
  const starredRepos = this.stateReader.getStarredRepos();
199
208
  const preferredOrgs = this.stateReader.getPreferredOrgs();
200
- let searchPriority = 'normal';
209
+ let searchPriority = "normal";
201
210
  if (effectiveMergedCount > 0) {
202
- searchPriority = 'merged_pr';
211
+ searchPriority = "merged_pr";
203
212
  }
204
213
  else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
205
- searchPriority = 'preferred_org';
214
+ searchPriority = "preferred_org";
206
215
  }
207
216
  else if (starredRepos.includes(repoFullName)) {
208
- searchPriority = 'starred';
217
+ searchPriority = "starred";
209
218
  }
210
219
  const result = {
211
220
  issue: trackedIssue,
@@ -218,7 +227,7 @@ export class IssueVetter {
218
227
  searchPriority,
219
228
  };
220
229
  // Cache the vetting result to avoid redundant API calls on repeated searches
221
- cache.set(cacheKey, '', result);
230
+ cache.set(cacheKey, "", result);
222
231
  return result;
223
232
  }
224
233
  /**
@@ -265,6 +274,10 @@ export class IssueVetter {
265
274
  warn(MODULE, `All ${attemptedCount} issue(s) failed vetting. ` +
266
275
  `This may indicate a systemic issue (rate limit, auth, network).`);
267
276
  }
268
- return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
277
+ return {
278
+ candidates: candidates.slice(0, maxResults),
279
+ allFailed,
280
+ rateLimitHit: rateLimitFailures > 0,
281
+ };
269
282
  }
270
283
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Local state persistence — reads/writes ScoutState to ~/.oss-scout/state.json.
3
3
  */
4
- import type { ScoutState } from './schemas.js';
4
+ import type { ScoutState } from "./schemas.js";
5
5
  /**
6
6
  * Check if a local state file exists.
7
7
  */
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Local state persistence — reads/writes ScoutState to ~/.oss-scout/state.json.
3
3
  */
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import { ScoutStateSchema } from './schemas.js';
7
- import { getDataDir } from './utils.js';
8
- import { debug, warn } from './logger.js';
9
- import { errorMessage } from './errors.js';
10
- const MODULE = 'local-state';
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { ScoutStateSchema } from "./schemas.js";
7
+ import { getDataDir } from "./utils.js";
8
+ import { debug, warn } from "./logger.js";
9
+ import { errorMessage } from "./errors.js";
10
+ const MODULE = "local-state";
11
11
  function getStatePath() {
12
- return path.join(getDataDir(), 'state.json');
12
+ return path.join(getDataDir(), "state.json");
13
13
  }
14
14
  /**
15
15
  * Check if a local state file exists.
@@ -23,12 +23,12 @@ export function hasLocalState() {
23
23
  export function loadLocalState() {
24
24
  const statePath = getStatePath();
25
25
  try {
26
- const raw = fs.readFileSync(statePath, 'utf-8');
26
+ const raw = fs.readFileSync(statePath, "utf-8");
27
27
  return ScoutStateSchema.parse(JSON.parse(raw));
28
28
  }
29
29
  catch (err) {
30
30
  const code = err?.code;
31
- if (code === 'ENOENT') {
31
+ if (code === "ENOENT") {
32
32
  return ScoutStateSchema.parse({ version: 1 });
33
33
  }
34
34
  // State file exists but is corrupt or unreadable
@@ -39,7 +39,9 @@ export function loadLocalState() {
39
39
  fs.copyFileSync(statePath, backupPath);
40
40
  warn(MODULE, `Corrupt state backed up to ${backupPath}`);
41
41
  }
42
- catch { /* best effort backup */ }
42
+ catch {
43
+ /* best effort backup */
44
+ }
43
45
  return ScoutStateSchema.parse({ version: 1 });
44
46
  }
45
47
  }
@@ -48,9 +50,9 @@ export function loadLocalState() {
48
50
  */
49
51
  export function saveLocalState(state) {
50
52
  const statePath = getStatePath();
51
- const tmpPath = statePath + '.tmp';
52
- const data = JSON.stringify(state, null, 2) + '\n';
53
+ const tmpPath = statePath + ".tmp";
54
+ const data = JSON.stringify(state, null, 2) + "\n";
53
55
  fs.writeFileSync(tmpPath, data, { mode: 0o600 });
54
56
  fs.renameSync(tmpPath, statePath);
55
- debug(MODULE, 'State saved');
57
+ debug(MODULE, "State saved");
56
58
  }
@@ -4,8 +4,8 @@
4
4
  * Extracted from issue-vetting.ts to isolate repo-level checks
5
5
  * from issue-level eligibility logic.
6
6
  */
7
- import { Octokit } from '@octokit/rest';
8
- import { type ContributionGuidelines, type ProjectHealth } from './types.js';
7
+ import { Octokit } from "@octokit/rest";
8
+ import { type ContributionGuidelines, type ProjectHealth } from "./types.js";
9
9
  /**
10
10
  * Check the health of a GitHub project: recent commits, CI status, star/fork counts.
11
11
  * Results are cached for HEALTH_CACHE_TTL_MS (4 hours).