@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.
- package/dist/cli.bundle.cjs +42 -42
- package/dist/cli.js +110 -86
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +76 -72
- package/dist/commands/results.d.ts +1 -1
- package/dist/commands/results.js +1 -1
- package/dist/commands/search.d.ts +2 -2
- package/dist/commands/search.js +16 -6
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +27 -21
- package/dist/commands/validation.d.ts +1 -1
- package/dist/commands/validation.js +1 -1
- package/dist/commands/vet-list.d.ts +2 -2
- package/dist/commands/vet-list.js +12 -5
- package/dist/commands/vet.d.ts +3 -3
- package/dist/commands/vet.js +9 -5
- package/dist/core/bootstrap.d.ts +1 -1
- package/dist/core/bootstrap.js +20 -16
- package/dist/core/category-mapping.d.ts +1 -1
- package/dist/core/category-mapping.js +104 -13
- package/dist/core/errors.d.ts +8 -1
- package/dist/core/errors.js +31 -19
- package/dist/core/gist-state-store.d.ts +1 -1
- package/dist/core/gist-state-store.js +36 -27
- package/dist/core/github.d.ts +1 -1
- package/dist/core/github.js +5 -5
- package/dist/core/http-cache.js +26 -22
- package/dist/core/issue-discovery.d.ts +3 -3
- package/dist/core/issue-discovery.js +325 -277
- package/dist/core/issue-eligibility.d.ts +2 -2
- package/dist/core/issue-eligibility.js +26 -21
- package/dist/core/issue-filtering.js +23 -15
- package/dist/core/issue-scoring.js +1 -1
- package/dist/core/issue-vetting.d.ts +2 -2
- package/dist/core/issue-vetting.js +66 -53
- package/dist/core/local-state.d.ts +1 -1
- package/dist/core/local-state.js +16 -14
- package/dist/core/repo-health.d.ts +2 -2
- package/dist/core/repo-health.js +46 -35
- package/dist/core/schemas.d.ts +1 -1
- package/dist/core/schemas.js +40 -18
- package/dist/core/search-budget.js +3 -3
- package/dist/core/search-phases.d.ts +6 -6
- package/dist/core/search-phases.js +23 -19
- package/dist/core/types.d.ts +9 -9
- package/dist/core/types.js +15 -3
- package/dist/core/utils.d.ts +10 -1
- package/dist/core/utils.js +44 -25
- package/dist/formatters/json.d.ts +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -5
- package/dist/scout.d.ts +4 -5
- package/dist/scout.js +72 -31
- package/package.json +1 -1
package/dist/core/repo-health.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Extracted from issue-vetting.ts to isolate repo-level checks
|
|
5
5
|
* from issue-level eligibility logic.
|
|
6
6
|
*/
|
|
7
|
-
import { daysBetween } from
|
|
8
|
-
import { errorMessage } from
|
|
9
|
-
import { warn } from
|
|
10
|
-
import { getHttpCache, cachedRequest, cachedTimeBased } from
|
|
11
|
-
const MODULE =
|
|
7
|
+
import { daysBetween } from "./utils.js";
|
|
8
|
+
import { errorMessage } from "./errors.js";
|
|
9
|
+
import { warn } from "./logger.js";
|
|
10
|
+
import { getHttpCache, cachedRequest, cachedTimeBased } from "./http-cache.js";
|
|
11
|
+
const MODULE = "repo-health";
|
|
12
12
|
// ── Cache for contribution guidelines ──
|
|
13
13
|
const guidelinesCache = new Map();
|
|
14
14
|
/** TTL for cached contribution guidelines (1 hour). */
|
|
@@ -57,7 +57,7 @@ export async function checkProjectHealth(octokit, owner, repo) {
|
|
|
57
57
|
const lastCommit = commits[0];
|
|
58
58
|
const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
|
|
59
59
|
const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
|
|
60
|
-
const ciStatus =
|
|
60
|
+
const ciStatus = "unknown";
|
|
61
61
|
return {
|
|
62
62
|
repo: `${owner}/${repo}`,
|
|
63
63
|
lastCommitAt,
|
|
@@ -77,11 +77,11 @@ export async function checkProjectHealth(octokit, owner, repo) {
|
|
|
77
77
|
warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
78
78
|
return {
|
|
79
79
|
repo: `${owner}/${repo}`,
|
|
80
|
-
lastCommitAt:
|
|
80
|
+
lastCommitAt: "",
|
|
81
81
|
daysSinceLastCommit: 999,
|
|
82
82
|
openIssuesCount: 0,
|
|
83
83
|
avgIssueResponseDays: 0,
|
|
84
|
-
ciStatus:
|
|
84
|
+
ciStatus: "unknown",
|
|
85
85
|
isActive: false,
|
|
86
86
|
checkFailed: true,
|
|
87
87
|
failureReason: errMsg,
|
|
@@ -101,31 +101,41 @@ export async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
101
101
|
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
102
102
|
return cached.guidelines;
|
|
103
103
|
}
|
|
104
|
-
const filesToCheck = [
|
|
104
|
+
const filesToCheck = [
|
|
105
|
+
"CONTRIBUTING.md",
|
|
106
|
+
".github/CONTRIBUTING.md",
|
|
107
|
+
"docs/CONTRIBUTING.md",
|
|
108
|
+
"contributing.md",
|
|
109
|
+
];
|
|
105
110
|
// Probe all paths in parallel — take the first success in priority order
|
|
106
111
|
const results = await Promise.allSettled(filesToCheck.map((file) => octokit.repos.getContent({ owner, repo, path: file }).then(({ data }) => {
|
|
107
|
-
if (
|
|
108
|
-
return Buffer.from(data.content,
|
|
112
|
+
if ("content" in data) {
|
|
113
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
109
114
|
}
|
|
110
115
|
return null;
|
|
111
116
|
})));
|
|
112
117
|
for (let i = 0; i < results.length; i++) {
|
|
113
118
|
const result = results[i];
|
|
114
|
-
if (result.status ===
|
|
119
|
+
if (result.status === "fulfilled" && result.value) {
|
|
115
120
|
const guidelines = parseContributionGuidelines(result.value);
|
|
116
121
|
guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
|
|
117
122
|
pruneCache();
|
|
118
123
|
return guidelines;
|
|
119
124
|
}
|
|
120
|
-
if (result.status ===
|
|
121
|
-
const msg = result.reason instanceof Error
|
|
122
|
-
|
|
125
|
+
if (result.status === "rejected") {
|
|
126
|
+
const msg = result.reason instanceof Error
|
|
127
|
+
? result.reason.message
|
|
128
|
+
: String(result.reason);
|
|
129
|
+
if (!msg.includes("404") && !msg.includes("Not Found")) {
|
|
123
130
|
warn(MODULE, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${msg}`);
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
// Cache the negative result too and prune if needed
|
|
128
|
-
guidelinesCache.set(cacheKey, {
|
|
135
|
+
guidelinesCache.set(cacheKey, {
|
|
136
|
+
guidelines: undefined,
|
|
137
|
+
fetchedAt: Date.now(),
|
|
138
|
+
});
|
|
129
139
|
pruneCache();
|
|
130
140
|
return undefined;
|
|
131
141
|
}
|
|
@@ -139,40 +149,41 @@ function parseContributionGuidelines(content) {
|
|
|
139
149
|
};
|
|
140
150
|
const lowerContent = content.toLowerCase();
|
|
141
151
|
// Detect branch naming conventions
|
|
142
|
-
if (lowerContent.includes(
|
|
152
|
+
if (lowerContent.includes("branch")) {
|
|
143
153
|
const branchMatch = content.match(/branch[^\n]*(?:named?|format|convention)[^\n]*[`"]([^`"]+)[`"]/i);
|
|
144
154
|
if (branchMatch) {
|
|
145
155
|
guidelines.branchNamingConvention = branchMatch[1];
|
|
146
156
|
}
|
|
147
157
|
}
|
|
148
158
|
// Detect commit message format
|
|
149
|
-
if (lowerContent.includes(
|
|
150
|
-
guidelines.commitMessageFormat =
|
|
159
|
+
if (lowerContent.includes("conventional commit")) {
|
|
160
|
+
guidelines.commitMessageFormat = "conventional commits";
|
|
151
161
|
}
|
|
152
|
-
else if (lowerContent.includes(
|
|
162
|
+
else if (lowerContent.includes("commit message")) {
|
|
153
163
|
const commitMatch = content.match(/commit message[^\n]*[`"]([^`"]+)[`"]/i);
|
|
154
164
|
if (commitMatch) {
|
|
155
165
|
guidelines.commitMessageFormat = commitMatch[1];
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
// Detect test framework
|
|
159
|
-
if (lowerContent.includes(
|
|
160
|
-
guidelines.testFramework =
|
|
161
|
-
else if (lowerContent.includes(
|
|
162
|
-
guidelines.testFramework =
|
|
163
|
-
else if (lowerContent.includes(
|
|
164
|
-
guidelines.testFramework =
|
|
165
|
-
else if (lowerContent.includes(
|
|
166
|
-
guidelines.testFramework =
|
|
169
|
+
if (lowerContent.includes("jest"))
|
|
170
|
+
guidelines.testFramework = "Jest";
|
|
171
|
+
else if (lowerContent.includes("rspec"))
|
|
172
|
+
guidelines.testFramework = "RSpec";
|
|
173
|
+
else if (lowerContent.includes("pytest"))
|
|
174
|
+
guidelines.testFramework = "pytest";
|
|
175
|
+
else if (lowerContent.includes("mocha"))
|
|
176
|
+
guidelines.testFramework = "Mocha";
|
|
167
177
|
// Detect linter
|
|
168
|
-
if (lowerContent.includes(
|
|
169
|
-
guidelines.linter =
|
|
170
|
-
else if (lowerContent.includes(
|
|
171
|
-
guidelines.linter =
|
|
172
|
-
else if (lowerContent.includes(
|
|
173
|
-
guidelines.formatter =
|
|
178
|
+
if (lowerContent.includes("eslint"))
|
|
179
|
+
guidelines.linter = "ESLint";
|
|
180
|
+
else if (lowerContent.includes("rubocop"))
|
|
181
|
+
guidelines.linter = "RuboCop";
|
|
182
|
+
else if (lowerContent.includes("prettier"))
|
|
183
|
+
guidelines.formatter = "Prettier";
|
|
174
184
|
// Detect CLA requirement
|
|
175
|
-
if (lowerContent.includes(
|
|
185
|
+
if (lowerContent.includes("cla") ||
|
|
186
|
+
lowerContent.includes("contributor license agreement")) {
|
|
176
187
|
guidelines.claRequired = true;
|
|
177
188
|
}
|
|
178
189
|
return guidelines;
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This file is the single source of truth for persisted type shapes.
|
|
5
5
|
* Types are inferred via `z.infer<>` at the bottom.
|
|
6
6
|
*/
|
|
7
|
-
import { z } from
|
|
7
|
+
import { z } from "zod";
|
|
8
8
|
export declare const IssueStatusSchema: z.ZodEnum<{
|
|
9
9
|
candidate: "candidate";
|
|
10
10
|
claimed: "claimed";
|
package/dist/core/schemas.js
CHANGED
|
@@ -4,21 +4,43 @@
|
|
|
4
4
|
* This file is the single source of truth for persisted type shapes.
|
|
5
5
|
* Types are inferred via `z.infer<>` at the bottom.
|
|
6
6
|
*/
|
|
7
|
-
import { z } from
|
|
7
|
+
import { z } from "zod";
|
|
8
8
|
// ── Enum schemas ────────────────────────────────────────────────────
|
|
9
|
-
export const IssueStatusSchema = z.enum([
|
|
9
|
+
export const IssueStatusSchema = z.enum([
|
|
10
|
+
"candidate",
|
|
11
|
+
"claimed",
|
|
12
|
+
"in_progress",
|
|
13
|
+
"pr_submitted",
|
|
14
|
+
]);
|
|
10
15
|
export const ProjectCategorySchema = z.enum([
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
"nonprofit",
|
|
17
|
+
"devtools",
|
|
18
|
+
"infrastructure",
|
|
19
|
+
"web-frameworks",
|
|
20
|
+
"data-ml",
|
|
21
|
+
"education",
|
|
22
|
+
]);
|
|
23
|
+
export const IssueScopeSchema = z.enum([
|
|
24
|
+
"beginner",
|
|
25
|
+
"intermediate",
|
|
26
|
+
"advanced",
|
|
27
|
+
]);
|
|
28
|
+
export const SearchStrategySchema = z.enum([
|
|
29
|
+
"merged",
|
|
30
|
+
"orgs",
|
|
31
|
+
"starred",
|
|
32
|
+
"broad",
|
|
33
|
+
"maintained",
|
|
34
|
+
"all",
|
|
17
35
|
]);
|
|
18
|
-
export const IssueScopeSchema = z.enum(['beginner', 'intermediate', 'advanced']);
|
|
19
|
-
export const SearchStrategySchema = z.enum(['merged', 'orgs', 'starred', 'broad', 'maintained', 'all']);
|
|
20
36
|
/** All concrete strategies (excludes 'all' meta-strategy). */
|
|
21
|
-
export const CONCRETE_STRATEGIES = [
|
|
37
|
+
export const CONCRETE_STRATEGIES = [
|
|
38
|
+
"merged",
|
|
39
|
+
"orgs",
|
|
40
|
+
"starred",
|
|
41
|
+
"broad",
|
|
42
|
+
"maintained",
|
|
43
|
+
];
|
|
22
44
|
// ── Leaf schemas ────────────────────────────────────────────────────
|
|
23
45
|
export const RepoSignalsSchema = z.object({
|
|
24
46
|
hasActiveMaintainers: z.boolean(),
|
|
@@ -96,7 +118,7 @@ export const SavedCandidateSchema = z.object({
|
|
|
96
118
|
number: z.number(),
|
|
97
119
|
title: z.string(),
|
|
98
120
|
labels: z.array(z.string()),
|
|
99
|
-
recommendation: z.enum([
|
|
121
|
+
recommendation: z.enum(["approve", "skip", "needs_review"]),
|
|
100
122
|
viabilityScore: z.number(),
|
|
101
123
|
searchPriority: z.string(),
|
|
102
124
|
firstSeenAt: z.string(),
|
|
@@ -104,22 +126,22 @@ export const SavedCandidateSchema = z.object({
|
|
|
104
126
|
lastScore: z.number(),
|
|
105
127
|
});
|
|
106
128
|
// ── Scout preferences schema ────────────────────────────────────────
|
|
107
|
-
export const PersistenceModeSchema = z.enum([
|
|
129
|
+
export const PersistenceModeSchema = z.enum(["local", "gist"]);
|
|
108
130
|
export const ScoutPreferencesSchema = z.object({
|
|
109
|
-
githubUsername: z.string().default(
|
|
110
|
-
languages: z.array(z.string()).default([
|
|
111
|
-
labels: z.array(z.string()).default([
|
|
131
|
+
githubUsername: z.string().default(""),
|
|
132
|
+
languages: z.array(z.string()).default(["typescript", "javascript"]),
|
|
133
|
+
labels: z.array(z.string()).default(["good first issue", "help wanted"]),
|
|
112
134
|
scope: z.array(IssueScopeSchema).optional(),
|
|
113
135
|
excludeRepos: z.array(z.string()).default([]),
|
|
114
136
|
excludeOrgs: z.array(z.string()).default([]),
|
|
115
|
-
aiPolicyBlocklist: z.array(z.string()).default([
|
|
137
|
+
aiPolicyBlocklist: z.array(z.string()).default(["matplotlib/matplotlib"]),
|
|
116
138
|
preferredOrgs: z.array(z.string()).default([]),
|
|
117
139
|
projectCategories: z.array(ProjectCategorySchema).default([]),
|
|
118
140
|
minStars: z.number().default(50),
|
|
119
141
|
maxIssueAgeDays: z.number().default(90),
|
|
120
142
|
includeDocIssues: z.boolean().default(true),
|
|
121
143
|
minRepoScoreThreshold: z.number().default(4),
|
|
122
|
-
persistence: PersistenceModeSchema.default(
|
|
144
|
+
persistence: PersistenceModeSchema.default("local"),
|
|
123
145
|
defaultStrategy: z.array(SearchStrategySchema).optional(),
|
|
124
146
|
});
|
|
125
147
|
// ── Root state schema ───────────────────────────────────────────────
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - Call waitForBudget() before making a Search API call to pace requests
|
|
12
12
|
* - Call canAfford(n) to check if n more calls fit in the remaining budget
|
|
13
13
|
*/
|
|
14
|
-
import { debug } from
|
|
15
|
-
import { sleep } from
|
|
16
|
-
const MODULE =
|
|
14
|
+
import { debug } from "./logger.js";
|
|
15
|
+
import { sleep } from "./utils.js";
|
|
16
|
+
const MODULE = "search-budget";
|
|
17
17
|
/** GitHub Search API rate limit: 30 requests per 60-second rolling window. */
|
|
18
18
|
const SEARCH_RATE_LIMIT = 30;
|
|
19
19
|
const SEARCH_WINDOW_MS = 60 * 1000;
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Extracted from issue-discovery.ts to isolate search helpers,
|
|
5
5
|
* caching, spam-filtering, and batched repo search logic.
|
|
6
6
|
*/
|
|
7
|
-
import { Octokit } from
|
|
8
|
-
import { type SearchPriority, type IssueCandidate, type IssueScope } from
|
|
9
|
-
import { type GitHubSearchItem } from
|
|
10
|
-
import { IssueVetter } from
|
|
7
|
+
import { Octokit } from "@octokit/rest";
|
|
8
|
+
import { type SearchPriority, type IssueCandidate, type IssueScope } from "./types.js";
|
|
9
|
+
import { type GitHubSearchItem } from "./issue-filtering.js";
|
|
10
|
+
import { IssueVetter } from "./issue-vetting.js";
|
|
11
11
|
/** Resolve scope tiers into a flat label list, merged with custom labels. */
|
|
12
12
|
export declare function buildEffectiveLabels(scopes: IssueScope[], customLabels: string[]): string[];
|
|
13
13
|
/** Round-robin interleave multiple arrays. */
|
|
@@ -19,8 +19,8 @@ export declare function interleaveArrays<T>(arrays: T[][]): T[];
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function cachedSearchIssues(octokit: Octokit, params: {
|
|
21
21
|
q: string;
|
|
22
|
-
sort:
|
|
23
|
-
order:
|
|
22
|
+
sort: "created" | "updated" | "comments" | "reactions" | "interactions";
|
|
23
|
+
order: "asc" | "desc";
|
|
24
24
|
per_page: number;
|
|
25
25
|
}): Promise<{
|
|
26
26
|
total_count: number;
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Extracted from issue-discovery.ts to isolate search helpers,
|
|
5
5
|
* caching, spam-filtering, and batched repo search logic.
|
|
6
6
|
*/
|
|
7
|
-
import { SCOPE_LABELS } from
|
|
8
|
-
import { errorMessage, isRateLimitError } from
|
|
9
|
-
import { debug, warn } from
|
|
10
|
-
import { getHttpCache, cachedTimeBased } from
|
|
11
|
-
import { detectLabelFarmingRepos } from
|
|
12
|
-
import { sleep } from
|
|
13
|
-
import { getSearchBudgetTracker } from
|
|
14
|
-
const MODULE =
|
|
7
|
+
import { SCOPE_LABELS, } from "./types.js";
|
|
8
|
+
import { errorMessage, isRateLimitError } from "./errors.js";
|
|
9
|
+
import { debug, warn } from "./logger.js";
|
|
10
|
+
import { getHttpCache, cachedTimeBased } from "./http-cache.js";
|
|
11
|
+
import { detectLabelFarmingRepos, } from "./issue-filtering.js";
|
|
12
|
+
import { extractRepoFromUrl, sleep } from "./utils.js";
|
|
13
|
+
import { getSearchBudgetTracker } from "./search-budget.js";
|
|
14
|
+
const MODULE = "search-phases";
|
|
15
15
|
/** GitHub Search API enforces a max of 5 AND/OR/NOT operators per query. */
|
|
16
16
|
const GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
17
17
|
/** Delay between search API calls to avoid GitHub's secondary rate limit (~30 req/min).
|
|
@@ -49,10 +49,10 @@ function chunkLabels(labels, reservedOps = 0) {
|
|
|
49
49
|
/** Build a GitHub Search API label filter from a list of labels. */
|
|
50
50
|
function buildLabelQuery(labels) {
|
|
51
51
|
if (labels.length === 0)
|
|
52
|
-
return
|
|
52
|
+
return "";
|
|
53
53
|
if (labels.length === 1)
|
|
54
54
|
return `label:"${labels[0]}"`;
|
|
55
|
-
return `(${labels.map((l) => `label:"${l}"`).join(
|
|
55
|
+
return `(${labels.map((l) => `label:"${l}"`).join(" OR ")})`;
|
|
56
56
|
}
|
|
57
57
|
/** Resolve scope tiers into a flat label list, merged with custom labels. */
|
|
58
58
|
export function buildEffectiveLabels(scopes, customLabels) {
|
|
@@ -132,8 +132,8 @@ export async function searchWithChunkedLabels(octokit, labels, reservedOps, buil
|
|
|
132
132
|
const query = buildQuery(buildLabelQuery(labelChunks[i]));
|
|
133
133
|
const data = await cachedSearchIssues(octokit, {
|
|
134
134
|
q: query,
|
|
135
|
-
sort:
|
|
136
|
-
order:
|
|
135
|
+
sort: "created",
|
|
136
|
+
order: "desc",
|
|
137
137
|
per_page: perPage,
|
|
138
138
|
});
|
|
139
139
|
for (const item of data.items) {
|
|
@@ -152,12 +152,14 @@ export async function searchWithChunkedLabels(octokit, labels, reservedOps, buil
|
|
|
152
152
|
export async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
|
|
153
153
|
const spamRepos = detectLabelFarmingRepos(items);
|
|
154
154
|
if (spamRepos.size > 0) {
|
|
155
|
-
const spamCount = items.filter((i) => spamRepos.has(i.repository_url
|
|
156
|
-
debug(MODULE, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(
|
|
155
|
+
const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
|
|
156
|
+
debug(MODULE, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
|
|
157
157
|
}
|
|
158
158
|
const itemsToVet = filterIssues(items)
|
|
159
159
|
.filter((item) => {
|
|
160
|
-
const repoFullName = item.repository_url
|
|
160
|
+
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
161
|
+
if (!repoFullName)
|
|
162
|
+
return false;
|
|
161
163
|
if (spamRepos.has(repoFullName))
|
|
162
164
|
return false;
|
|
163
165
|
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
@@ -167,7 +169,7 @@ export async function filterVetAndScore(vetter, items, filterIssues, excludedRep
|
|
|
167
169
|
debug(MODULE, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
168
170
|
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
169
171
|
}
|
|
170
|
-
const { candidates: results, allFailed: allVetFailed, rateLimitHit, } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded,
|
|
172
|
+
const { candidates: results, allFailed: allVetFailed, rateLimitHit, } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
|
|
171
173
|
const starFiltered = results.filter((c) => {
|
|
172
174
|
if (c.projectHealth.checkFailed)
|
|
173
175
|
return true;
|
|
@@ -206,10 +208,12 @@ export async function searchInRepos(octokit, vetter, repos, baseQualifiers, labe
|
|
|
206
208
|
if (batchIdx > 0)
|
|
207
209
|
await sleep(INTER_QUERY_DELAY_MS);
|
|
208
210
|
try {
|
|
209
|
-
const repoFilter = batch.map((r) => `repo:${r}`).join(
|
|
211
|
+
const repoFilter = batch.map((r) => `repo:${r}`).join(" OR ");
|
|
210
212
|
const repoOps = batch.length - 1;
|
|
211
213
|
const perPage = Math.min(30, (maxResults - candidates.length) * 3);
|
|
212
|
-
const allItems = await searchWithChunkedLabels(octokit, labels, repoOps, (labelQ) => `${baseQualifiers} ${labelQ} (${repoFilter})
|
|
214
|
+
const allItems = await searchWithChunkedLabels(octokit, labels, repoOps, (labelQ) => `${baseQualifiers} ${labelQ} (${repoFilter})`
|
|
215
|
+
.replace(/ +/g, " ")
|
|
216
|
+
.trim(), perPage);
|
|
213
217
|
if (allItems.length > 0) {
|
|
214
218
|
const filtered = filterFn(allItems);
|
|
215
219
|
const remainingNeeded = maxResults - candidates.length;
|
|
@@ -224,7 +228,7 @@ export async function searchInRepos(octokit, vetter, repos, baseQualifiers, labe
|
|
|
224
228
|
if (isRateLimitError(error)) {
|
|
225
229
|
rateLimitFailures++;
|
|
226
230
|
}
|
|
227
|
-
const batchReposStr = batch.join(
|
|
231
|
+
const batchReposStr = batch.join(", ");
|
|
228
232
|
warn(MODULE, `Error searching issues in batch [${batchReposStr}]:`, errorMessage(error));
|
|
229
233
|
}
|
|
230
234
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core types for oss-scout — ephemeral types that are never persisted.
|
|
3
3
|
*/
|
|
4
|
-
import type { RepoSignals, TrackedIssue, IssueVettingResult, IssueScope, ScoutState, SearchStrategy } from
|
|
5
|
-
export type { ProjectCategory, IssueScope, RepoSignals, RepoScore, StoredMergedPR, StoredClosedPR, ContributionGuidelines, IssueVettingResult, TrackedIssue, ScoutPreferences, SavedCandidate, ScoutState, SearchStrategy, } from
|
|
4
|
+
import type { RepoSignals, TrackedIssue, IssueVettingResult, IssueScope, ScoutState, SearchStrategy } from "./schemas.js";
|
|
5
|
+
export type { ProjectCategory, IssueScope, RepoSignals, RepoScore, StoredMergedPR, StoredClosedPR, ContributionGuidelines, IssueVettingResult, TrackedIssue, ScoutPreferences, SavedCandidate, ScoutState, SearchStrategy, } from "./schemas.js";
|
|
6
6
|
/** Health snapshot of a GitHub repository. */
|
|
7
7
|
export interface ProjectHealth {
|
|
8
8
|
repo: string;
|
|
@@ -10,7 +10,7 @@ export interface ProjectHealth {
|
|
|
10
10
|
daysSinceLastCommit: number;
|
|
11
11
|
openIssuesCount: number;
|
|
12
12
|
avgIssueResponseDays: number;
|
|
13
|
-
ciStatus:
|
|
13
|
+
ciStatus: "passing" | "failing" | "unknown";
|
|
14
14
|
isActive: boolean;
|
|
15
15
|
stargazersCount?: number;
|
|
16
16
|
forksCount?: number;
|
|
@@ -19,13 +19,13 @@ export interface ProjectHealth {
|
|
|
19
19
|
failureReason?: string;
|
|
20
20
|
}
|
|
21
21
|
/** Priority tier for issue search results. */
|
|
22
|
-
export type SearchPriority =
|
|
22
|
+
export type SearchPriority = "merged_pr" | "preferred_org" | "starred" | "normal";
|
|
23
23
|
/** A fully vetted issue candidate with scoring. */
|
|
24
24
|
export interface IssueCandidate {
|
|
25
25
|
issue: TrackedIssue;
|
|
26
26
|
vettingResult: IssueVettingResult;
|
|
27
27
|
projectHealth: ProjectHealth;
|
|
28
|
-
recommendation:
|
|
28
|
+
recommendation: "approve" | "skip" | "needs_review";
|
|
29
29
|
reasonsToSkip: string[];
|
|
30
30
|
reasonsToApprove: string[];
|
|
31
31
|
viabilityScore: number;
|
|
@@ -59,8 +59,8 @@ export interface VetListEntry {
|
|
|
59
59
|
repo: string;
|
|
60
60
|
number: number;
|
|
61
61
|
title: string;
|
|
62
|
-
status:
|
|
63
|
-
recommendation?:
|
|
62
|
+
status: "still_available" | "claimed" | "closed" | "has_pr" | "error";
|
|
63
|
+
recommendation?: "approve" | "skip" | "needs_review";
|
|
64
64
|
viabilityScore?: number;
|
|
65
65
|
errorMessage?: string;
|
|
66
66
|
}
|
|
@@ -84,14 +84,14 @@ export type ScoutConfig = {
|
|
|
84
84
|
/** GitHub token with `repo` read scope. Add `gist` scope for persistence. */
|
|
85
85
|
githubToken: string;
|
|
86
86
|
/** Use gist-backed persistence (default for standalone CLI). */
|
|
87
|
-
persistence?:
|
|
87
|
+
persistence?: "gist";
|
|
88
88
|
/** Gist ID override. Skips gist discovery/creation if provided. */
|
|
89
89
|
gistId?: string;
|
|
90
90
|
} | {
|
|
91
91
|
/** GitHub token with `repo` read scope. */
|
|
92
92
|
githubToken: string;
|
|
93
93
|
/** Caller provides state directly. */
|
|
94
|
-
persistence:
|
|
94
|
+
persistence: "provided";
|
|
95
95
|
/** Pre-loaded state. Required when persistence is 'provided'. */
|
|
96
96
|
initialState: ScoutState;
|
|
97
97
|
};
|
package/dist/core/types.js
CHANGED
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
// ── Const arrays and mappings ───────────────────────────────────────
|
|
5
5
|
export const SCOPE_LABELS = {
|
|
6
|
-
beginner: [
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
beginner: [
|
|
7
|
+
"good first issue",
|
|
8
|
+
"help wanted",
|
|
9
|
+
"easy",
|
|
10
|
+
"up-for-grabs",
|
|
11
|
+
"first-timers-only",
|
|
12
|
+
"beginner",
|
|
13
|
+
],
|
|
14
|
+
intermediate: [
|
|
15
|
+
"enhancement",
|
|
16
|
+
"feature",
|
|
17
|
+
"feature-request",
|
|
18
|
+
"contributions welcome",
|
|
19
|
+
],
|
|
20
|
+
advanced: ["proposal", "RFC", "accepted", "design"],
|
|
9
21
|
};
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
export declare function sleep(ms: number): Promise<void>;
|
|
5
5
|
export declare function getDataDir(): string;
|
|
6
6
|
export declare function getCacheDir(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Extract "owner/repo" from any GitHub URL format:
|
|
9
|
+
* - https://github.com/owner/repo
|
|
10
|
+
* - https://github.com/owner/repo/pull/123
|
|
11
|
+
* - https://github.com/owner/repo/issues/123
|
|
12
|
+
* - https://api.github.com/repos/owner/repo
|
|
13
|
+
* - https://api.github.com/repos/owner/repo/...
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractRepoFromUrl(url: string): string | null;
|
|
7
16
|
interface ParsedGitHubUrl {
|
|
8
17
|
owner: string;
|
|
9
18
|
repo: string;
|
|
10
19
|
number: number;
|
|
11
|
-
type:
|
|
20
|
+
type: "pull" | "issues";
|
|
12
21
|
}
|
|
13
22
|
export declare function parseGitHubUrl(url: string): ParsedGitHubUrl | null;
|
|
14
23
|
export declare function daysBetween(from: Date, to?: Date): number;
|
package/dist/core/utils.js
CHANGED
|
@@ -1,39 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared utility functions for oss-scout.
|
|
3
3
|
*/
|
|
4
|
-
import * as fs from
|
|
5
|
-
import * as path from
|
|
6
|
-
import * as os from
|
|
7
|
-
import { execFileSync } from
|
|
8
|
-
import { ConfigurationError, errorMessage } from
|
|
9
|
-
import { debug } from
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { ConfigurationError, errorMessage } from "./errors.js";
|
|
9
|
+
import { debug } from "./logger.js";
|
|
10
10
|
export function sleep(ms) {
|
|
11
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
12
|
}
|
|
13
|
-
const MODULE =
|
|
13
|
+
const MODULE = "utils";
|
|
14
14
|
let cachedGitHubToken = null;
|
|
15
15
|
let tokenFetchAttempted = false;
|
|
16
16
|
export function getDataDir() {
|
|
17
|
-
const dir = path.join(os.homedir(),
|
|
17
|
+
const dir = path.join(os.homedir(), ".oss-scout");
|
|
18
18
|
if (!fs.existsSync(dir)) {
|
|
19
19
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
20
20
|
}
|
|
21
21
|
return dir;
|
|
22
22
|
}
|
|
23
23
|
export function getCacheDir() {
|
|
24
|
-
const dir = path.join(getDataDir(),
|
|
24
|
+
const dir = path.join(getDataDir(), "cache");
|
|
25
25
|
if (!fs.existsSync(dir)) {
|
|
26
26
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
27
27
|
}
|
|
28
28
|
return dir;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract "owner/repo" from any GitHub URL format:
|
|
32
|
+
* - https://github.com/owner/repo
|
|
33
|
+
* - https://github.com/owner/repo/pull/123
|
|
34
|
+
* - https://github.com/owner/repo/issues/123
|
|
35
|
+
* - https://api.github.com/repos/owner/repo
|
|
36
|
+
* - https://api.github.com/repos/owner/repo/...
|
|
37
|
+
*/
|
|
38
|
+
export function extractRepoFromUrl(url) {
|
|
39
|
+
// API URLs: https://api.github.com/repos/owner/repo[/...]
|
|
40
|
+
const apiMatch = url.match(/api\.github\.com\/repos\/([^/]+\/[^/]+)/);
|
|
41
|
+
if (apiMatch)
|
|
42
|
+
return apiMatch[1];
|
|
43
|
+
// Web URLs: https://github.com/owner/repo[/...]
|
|
44
|
+
const webMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
45
|
+
if (webMatch)
|
|
46
|
+
return webMatch[1];
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
30
49
|
const OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
31
50
|
const REPO_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
32
51
|
function isValidOwnerRepo(owner, repo) {
|
|
33
52
|
return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
|
|
34
53
|
}
|
|
35
54
|
export function parseGitHubUrl(url) {
|
|
36
|
-
if (!url.startsWith(
|
|
55
|
+
if (!url.startsWith("https://github.com/"))
|
|
37
56
|
return null;
|
|
38
57
|
const prMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
39
58
|
if (prMatch) {
|
|
@@ -41,7 +60,7 @@ export function parseGitHubUrl(url) {
|
|
|
41
60
|
const repo = prMatch[2];
|
|
42
61
|
if (!isValidOwnerRepo(owner, repo))
|
|
43
62
|
return null;
|
|
44
|
-
return { owner, repo, number: parseInt(prMatch[3], 10), type:
|
|
63
|
+
return { owner, repo, number: parseInt(prMatch[3], 10), type: "pull" };
|
|
45
64
|
}
|
|
46
65
|
const issueMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
47
66
|
if (issueMatch) {
|
|
@@ -49,7 +68,7 @@ export function parseGitHubUrl(url) {
|
|
|
49
68
|
const repo = issueMatch[2];
|
|
50
69
|
if (!isValidOwnerRepo(owner, repo))
|
|
51
70
|
return null;
|
|
52
|
-
return { owner, repo, number: parseInt(issueMatch[3], 10), type:
|
|
71
|
+
return { owner, repo, number: parseInt(issueMatch[3], 10), type: "issues" };
|
|
53
72
|
}
|
|
54
73
|
return null;
|
|
55
74
|
}
|
|
@@ -58,12 +77,12 @@ export function daysBetween(from, to = new Date()) {
|
|
|
58
77
|
}
|
|
59
78
|
export function getCLIVersion() {
|
|
60
79
|
try {
|
|
61
|
-
const pkgPath = path.join(path.dirname(process.argv[1]),
|
|
62
|
-
return JSON.parse(fs.readFileSync(pkgPath,
|
|
80
|
+
const pkgPath = path.join(path.dirname(process.argv[1]), "..", "package.json");
|
|
81
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
63
82
|
}
|
|
64
83
|
catch (err) {
|
|
65
84
|
debug(MODULE, `Could not read CLI version: ${errorMessage(err)}`);
|
|
66
|
-
return
|
|
85
|
+
return "unknown";
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
88
|
export function getGitHubToken() {
|
|
@@ -77,30 +96,30 @@ export function getGitHubToken() {
|
|
|
77
96
|
return cachedGitHubToken;
|
|
78
97
|
}
|
|
79
98
|
try {
|
|
80
|
-
const token = execFileSync(
|
|
81
|
-
encoding:
|
|
82
|
-
stdio: [
|
|
99
|
+
const token = execFileSync("gh", ["auth", "token"], {
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
83
102
|
timeout: 2000,
|
|
84
103
|
}).trim();
|
|
85
104
|
if (token && token.length > 0) {
|
|
86
105
|
cachedGitHubToken = token;
|
|
87
|
-
debug(MODULE,
|
|
106
|
+
debug(MODULE, "Using GitHub token from gh CLI");
|
|
88
107
|
return cachedGitHubToken;
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
catch (err) {
|
|
92
|
-
debug(MODULE,
|
|
111
|
+
debug(MODULE, "gh auth token failed", err);
|
|
93
112
|
}
|
|
94
113
|
return null;
|
|
95
114
|
}
|
|
96
115
|
export function requireGitHubToken() {
|
|
97
116
|
const token = getGitHubToken();
|
|
98
117
|
if (!token) {
|
|
99
|
-
throw new ConfigurationError(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
throw new ConfigurationError("GitHub authentication required.\n\n" +
|
|
119
|
+
"Options:\n" +
|
|
120
|
+
" 1. Use gh CLI: gh auth login\n" +
|
|
121
|
+
" 2. Set GITHUB_TOKEN environment variable\n\n" +
|
|
122
|
+
"The gh CLI is recommended - install from https://cli.github.com");
|
|
104
123
|
}
|
|
105
124
|
return token;
|
|
106
125
|
}
|