@oss-scout/core 0.2.0 → 0.3.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 +51 -47
- package/dist/cli.js +218 -87
- package/dist/commands/config.d.ts +2 -4
- package/dist/commands/config.js +76 -78
- 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 +25 -25
- package/dist/commands/skip.d.ts +33 -0
- package/dist/commands/skip.js +89 -0
- 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 +55 -28
- 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 +6 -6
- package/dist/core/issue-discovery.js +279 -286
- 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 -4
- package/dist/core/issue-vetting.js +65 -56
- 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 +17 -9
- package/dist/core/schemas.js +47 -19
- 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 +30 -6
- package/dist/scout.js +141 -34
- package/package.json +7 -3
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";
|
|
@@ -27,13 +27,12 @@ export declare const IssueScopeSchema: z.ZodEnum<{
|
|
|
27
27
|
export declare const SearchStrategySchema: z.ZodEnum<{
|
|
28
28
|
all: "all";
|
|
29
29
|
merged: "merged";
|
|
30
|
-
orgs: "orgs";
|
|
31
30
|
starred: "starred";
|
|
32
31
|
broad: "broad";
|
|
33
32
|
maintained: "maintained";
|
|
34
33
|
}>;
|
|
35
34
|
/** All concrete strategies (excludes 'all' meta-strategy). */
|
|
36
|
-
export declare const CONCRETE_STRATEGIES: readonly ["merged", "
|
|
35
|
+
export declare const CONCRETE_STRATEGIES: readonly ["merged", "starred", "broad", "maintained"];
|
|
37
36
|
export declare const RepoSignalsSchema: z.ZodObject<{
|
|
38
37
|
hasActiveMaintainers: z.ZodBoolean;
|
|
39
38
|
isResponsive: z.ZodBoolean;
|
|
@@ -152,6 +151,13 @@ export declare const TrackedIssueSchema: z.ZodObject<{
|
|
|
152
151
|
notes: z.ZodArray<z.ZodString>;
|
|
153
152
|
}, z.core.$strip>>;
|
|
154
153
|
}, z.core.$strip>;
|
|
154
|
+
export declare const SkippedIssueSchema: z.ZodObject<{
|
|
155
|
+
url: z.ZodString;
|
|
156
|
+
repo: z.ZodString;
|
|
157
|
+
number: z.ZodNumber;
|
|
158
|
+
title: z.ZodString;
|
|
159
|
+
skippedAt: z.ZodString;
|
|
160
|
+
}, z.core.$strip>;
|
|
155
161
|
export declare const SavedCandidateSchema: z.ZodObject<{
|
|
156
162
|
issueUrl: z.ZodString;
|
|
157
163
|
repo: z.ZodString;
|
|
@@ -185,7 +191,6 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
185
191
|
excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
186
192
|
excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
187
193
|
aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
188
|
-
preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
189
194
|
projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
190
195
|
nonprofit: "nonprofit";
|
|
191
196
|
devtools: "devtools";
|
|
@@ -205,7 +210,6 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
205
210
|
defaultStrategy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
206
211
|
all: "all";
|
|
207
212
|
merged: "merged";
|
|
208
|
-
orgs: "orgs";
|
|
209
213
|
starred: "starred";
|
|
210
214
|
broad: "broad";
|
|
211
215
|
maintained: "maintained";
|
|
@@ -225,7 +229,6 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
225
229
|
excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
226
230
|
excludeOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
227
231
|
aiPolicyBlocklist: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
228
|
-
preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
229
232
|
projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
230
233
|
nonprofit: "nonprofit";
|
|
231
234
|
devtools: "devtools";
|
|
@@ -245,7 +248,6 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
245
248
|
defaultStrategy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
246
249
|
all: "all";
|
|
247
250
|
merged: "merged";
|
|
248
|
-
orgs: "orgs";
|
|
249
251
|
starred: "starred";
|
|
250
252
|
broad: "broad";
|
|
251
253
|
maintained: "maintained";
|
|
@@ -296,11 +298,17 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
296
298
|
lastSeenAt: z.ZodString;
|
|
297
299
|
lastScore: z.ZodNumber;
|
|
298
300
|
}, z.core.$strip>>>;
|
|
301
|
+
skippedIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
302
|
+
url: z.ZodString;
|
|
303
|
+
repo: z.ZodString;
|
|
304
|
+
number: z.ZodNumber;
|
|
305
|
+
title: z.ZodString;
|
|
306
|
+
skippedAt: z.ZodString;
|
|
307
|
+
}, z.core.$strip>>>;
|
|
299
308
|
lastSearchAt: z.ZodOptional<z.ZodString>;
|
|
300
309
|
lastRunAt: z.ZodDefault<z.ZodString>;
|
|
301
310
|
gistId: z.ZodOptional<z.ZodString>;
|
|
302
311
|
}, z.core.$strip>;
|
|
303
|
-
export type IssueStatus = z.infer<typeof IssueStatusSchema>;
|
|
304
312
|
export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
|
|
305
313
|
export type IssueScope = z.infer<typeof IssueScopeSchema>;
|
|
306
314
|
export type SearchStrategy = z.infer<typeof SearchStrategySchema>;
|
|
@@ -311,7 +319,7 @@ export type StoredClosedPR = z.infer<typeof StoredClosedPRSchema>;
|
|
|
311
319
|
export type ContributionGuidelines = z.infer<typeof ContributionGuidelinesSchema>;
|
|
312
320
|
export type IssueVettingResult = z.infer<typeof IssueVettingResultSchema>;
|
|
313
321
|
export type TrackedIssue = z.infer<typeof TrackedIssueSchema>;
|
|
314
|
-
export type PersistenceMode = z.infer<typeof PersistenceModeSchema>;
|
|
315
322
|
export type ScoutPreferences = z.infer<typeof ScoutPreferencesSchema>;
|
|
316
323
|
export type SavedCandidate = z.infer<typeof SavedCandidateSchema>;
|
|
324
|
+
export type SkippedIssue = z.infer<typeof SkippedIssueSchema>;
|
|
317
325
|
export type ScoutState = z.infer<typeof ScoutStateSchema>;
|
package/dist/core/schemas.js
CHANGED
|
@@ -4,21 +4,41 @@
|
|
|
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
|
+
"starred",
|
|
31
|
+
"broad",
|
|
32
|
+
"maintained",
|
|
33
|
+
"all",
|
|
17
34
|
]);
|
|
18
|
-
export const IssueScopeSchema = z.enum(['beginner', 'intermediate', 'advanced']);
|
|
19
|
-
export const SearchStrategySchema = z.enum(['merged', 'orgs', 'starred', 'broad', 'maintained', 'all']);
|
|
20
35
|
/** All concrete strategies (excludes 'all' meta-strategy). */
|
|
21
|
-
export const CONCRETE_STRATEGIES = [
|
|
36
|
+
export const CONCRETE_STRATEGIES = [
|
|
37
|
+
"merged",
|
|
38
|
+
"starred",
|
|
39
|
+
"broad",
|
|
40
|
+
"maintained",
|
|
41
|
+
];
|
|
22
42
|
// ── Leaf schemas ────────────────────────────────────────────────────
|
|
23
43
|
export const RepoSignalsSchema = z.object({
|
|
24
44
|
hasActiveMaintainers: z.boolean(),
|
|
@@ -89,6 +109,14 @@ export const TrackedIssueSchema = z.object({
|
|
|
89
109
|
vetted: z.boolean(),
|
|
90
110
|
vettingResult: IssueVettingResultSchema.optional(),
|
|
91
111
|
});
|
|
112
|
+
// ── Skipped issue schema ──────────────────────────────────────────
|
|
113
|
+
export const SkippedIssueSchema = z.object({
|
|
114
|
+
url: z.string(),
|
|
115
|
+
repo: z.string(),
|
|
116
|
+
number: z.number(),
|
|
117
|
+
title: z.string(),
|
|
118
|
+
skippedAt: z.string(),
|
|
119
|
+
});
|
|
92
120
|
// ── Saved candidate schema ─────────────────────────────────────────
|
|
93
121
|
export const SavedCandidateSchema = z.object({
|
|
94
122
|
issueUrl: z.string(),
|
|
@@ -96,7 +124,7 @@ export const SavedCandidateSchema = z.object({
|
|
|
96
124
|
number: z.number(),
|
|
97
125
|
title: z.string(),
|
|
98
126
|
labels: z.array(z.string()),
|
|
99
|
-
recommendation: z.enum([
|
|
127
|
+
recommendation: z.enum(["approve", "skip", "needs_review"]),
|
|
100
128
|
viabilityScore: z.number(),
|
|
101
129
|
searchPriority: z.string(),
|
|
102
130
|
firstSeenAt: z.string(),
|
|
@@ -104,22 +132,21 @@ export const SavedCandidateSchema = z.object({
|
|
|
104
132
|
lastScore: z.number(),
|
|
105
133
|
});
|
|
106
134
|
// ── Scout preferences schema ────────────────────────────────────────
|
|
107
|
-
export const PersistenceModeSchema = z.enum([
|
|
135
|
+
export const PersistenceModeSchema = z.enum(["local", "gist"]);
|
|
108
136
|
export const ScoutPreferencesSchema = z.object({
|
|
109
|
-
githubUsername: z.string().default(
|
|
110
|
-
languages: z.array(z.string()).default([
|
|
111
|
-
labels: z.array(z.string()).default([
|
|
137
|
+
githubUsername: z.string().default(""),
|
|
138
|
+
languages: z.array(z.string()).default(["any"]),
|
|
139
|
+
labels: z.array(z.string()).default(["good first issue", "help wanted"]),
|
|
112
140
|
scope: z.array(IssueScopeSchema).optional(),
|
|
113
141
|
excludeRepos: z.array(z.string()).default([]),
|
|
114
142
|
excludeOrgs: z.array(z.string()).default([]),
|
|
115
|
-
aiPolicyBlocklist: z.array(z.string()).default([
|
|
116
|
-
preferredOrgs: z.array(z.string()).default([]),
|
|
143
|
+
aiPolicyBlocklist: z.array(z.string()).default(["matplotlib/matplotlib"]),
|
|
117
144
|
projectCategories: z.array(ProjectCategorySchema).default([]),
|
|
118
145
|
minStars: z.number().default(50),
|
|
119
146
|
maxIssueAgeDays: z.number().default(90),
|
|
120
147
|
includeDocIssues: z.boolean().default(true),
|
|
121
148
|
minRepoScoreThreshold: z.number().default(4),
|
|
122
|
-
persistence: PersistenceModeSchema.default(
|
|
149
|
+
persistence: PersistenceModeSchema.default("local"),
|
|
123
150
|
defaultStrategy: z.array(SearchStrategySchema).optional(),
|
|
124
151
|
});
|
|
125
152
|
// ── Root state schema ───────────────────────────────────────────────
|
|
@@ -132,6 +159,7 @@ export const ScoutStateSchema = z.object({
|
|
|
132
159
|
mergedPRs: z.array(StoredMergedPRSchema).default([]),
|
|
133
160
|
closedPRs: z.array(StoredClosedPRSchema).default([]),
|
|
134
161
|
savedResults: z.array(SavedCandidateSchema).default([]),
|
|
162
|
+
skippedIssues: z.array(SkippedIssueSchema).default([]),
|
|
135
163
|
lastSearchAt: z.string().optional(),
|
|
136
164
|
lastRunAt: z.string().default(() => new Date().toISOString()),
|
|
137
165
|
gistId: z.string().optional(),
|
|
@@ -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" | "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;
|