@oss-scout/core 0.2.1 → 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 +43 -39
- package/dist/cli.js +108 -1
- package/dist/commands/config.d.ts +1 -3
- package/dist/commands/config.js +1 -7
- package/dist/commands/setup.js +3 -9
- package/dist/commands/skip.d.ts +33 -0
- package/dist/commands/skip.js +89 -0
- package/dist/core/bootstrap.js +2 -2
- package/dist/core/gist-state-store.js +19 -1
- package/dist/core/issue-discovery.d.ts +3 -3
- package/dist/core/issue-discovery.js +12 -67
- package/dist/core/issue-vetting.d.ts +0 -2
- package/dist/core/issue-vetting.js +0 -4
- package/dist/core/local-state.js +2 -2
- package/dist/core/schemas.d.ts +16 -8
- package/dist/core/schemas.js +10 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/scout.d.ts +27 -2
- package/dist/scout.js +69 -3
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -223,7 +223,7 @@ const configCmd = program
|
|
|
223
223
|
console.log(formatJsonSuccess(getConfigData()));
|
|
224
224
|
}
|
|
225
225
|
else {
|
|
226
|
-
runConfigShow(
|
|
226
|
+
runConfigShow();
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
catch (err) {
|
|
@@ -322,6 +322,113 @@ program
|
|
|
322
322
|
handleCommandError(err, options);
|
|
323
323
|
}
|
|
324
324
|
});
|
|
325
|
+
// ── skip command ───────────────────────────────────────────────────
|
|
326
|
+
const skipCmd = program
|
|
327
|
+
.command("skip")
|
|
328
|
+
.description("Manage the skip list — exclude issues from future searches");
|
|
329
|
+
skipCmd
|
|
330
|
+
.command("add <issue-url>")
|
|
331
|
+
.description("Skip an issue by URL")
|
|
332
|
+
.option("--json", "Output as JSON")
|
|
333
|
+
.action(async (issueUrl, options) => {
|
|
334
|
+
try {
|
|
335
|
+
const { runSkip } = await import("./commands/skip.js");
|
|
336
|
+
const state = loadLocalState();
|
|
337
|
+
const result = await runSkip({ issueUrl, state });
|
|
338
|
+
if (options.json) {
|
|
339
|
+
console.log(formatJsonSuccess(result));
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
if (result.alreadySkipped) {
|
|
343
|
+
console.log("Issue already in skip list.");
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
console.log(`Skipped: ${issueUrl}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
handleCommandError(err, options);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
skipCmd
|
|
355
|
+
.command("list")
|
|
356
|
+
.description("Show all skipped issues")
|
|
357
|
+
.option("--json", "Output as JSON")
|
|
358
|
+
.action(async (options) => {
|
|
359
|
+
try {
|
|
360
|
+
const { runSkipList } = await import("./commands/skip.js");
|
|
361
|
+
const results = runSkipList();
|
|
362
|
+
if (options.json) {
|
|
363
|
+
console.log(formatJsonSuccess(results));
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
if (results.length === 0) {
|
|
367
|
+
console.log("\nNo skipped issues.\n");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
console.log(`\nSkipped issues (${results.length}):\n`);
|
|
371
|
+
console.log(" Repo Issue Skipped Title");
|
|
372
|
+
console.log(" ──────────────────────────────── ────── ────────── ─────");
|
|
373
|
+
for (const s of results) {
|
|
374
|
+
const repo = (s.repo || "unknown").padEnd(32).slice(0, 32);
|
|
375
|
+
const issue = s.number ? `#${s.number}`.padEnd(6) : "—".padEnd(6);
|
|
376
|
+
const skippedDate = s.skippedAt.split("T")[0] ?? "";
|
|
377
|
+
const title = s.title.length > 50
|
|
378
|
+
? s.title.slice(0, 47) + "..."
|
|
379
|
+
: s.title || s.url;
|
|
380
|
+
console.log(` ${repo} ${issue} ${skippedDate} ${title}`);
|
|
381
|
+
}
|
|
382
|
+
console.log();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
handleCommandError(err, options);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
skipCmd
|
|
390
|
+
.command("remove <issue-url>")
|
|
391
|
+
.description("Remove an issue from the skip list (unskip)")
|
|
392
|
+
.option("--json", "Output as JSON")
|
|
393
|
+
.action(async (issueUrl, options) => {
|
|
394
|
+
try {
|
|
395
|
+
const { runSkipRemove } = await import("./commands/skip.js");
|
|
396
|
+
const result = await runSkipRemove({ issueUrl });
|
|
397
|
+
if (options.json) {
|
|
398
|
+
console.log(formatJsonSuccess(result));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
if (result.removed) {
|
|
402
|
+
console.log(`Removed from skip list: ${issueUrl}`);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
console.log("Issue was not in the skip list.");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
handleCommandError(err, options);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
skipCmd
|
|
414
|
+
.command("clear")
|
|
415
|
+
.description("Clear all skipped issues")
|
|
416
|
+
.option("--json", "Output as JSON")
|
|
417
|
+
.action(async (options) => {
|
|
418
|
+
try {
|
|
419
|
+
const { runSkipClear } = await import("./commands/skip.js");
|
|
420
|
+
await runSkipClear();
|
|
421
|
+
if (options.json) {
|
|
422
|
+
console.log(formatJsonSuccess({ cleared: true }));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
console.log("Skip list cleared.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
handleCommandError(err, options);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
325
432
|
program
|
|
326
433
|
.command("vet <issue-url>")
|
|
327
434
|
.description("Vet a specific GitHub issue for claimability and project health")
|
|
@@ -5,9 +5,7 @@ import type { ScoutPreferences } from "../core/schemas.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Display current preferences in human-readable format.
|
|
7
7
|
*/
|
|
8
|
-
export declare function runConfigShow(
|
|
9
|
-
json?: boolean;
|
|
10
|
-
}): void;
|
|
8
|
+
export declare function runConfigShow(): void;
|
|
11
9
|
/**
|
|
12
10
|
* Get current preferences for JSON output.
|
|
13
11
|
*/
|
package/dist/commands/config.js
CHANGED
|
@@ -10,7 +10,6 @@ const FIELD_CONFIGS = {
|
|
|
10
10
|
excludeRepos: { type: "array" },
|
|
11
11
|
excludeOrgs: { type: "array" },
|
|
12
12
|
aiPolicyBlocklist: { type: "array" },
|
|
13
|
-
preferredOrgs: { type: "array" },
|
|
14
13
|
minStars: { type: "number" },
|
|
15
14
|
maxIssueAgeDays: { type: "number" },
|
|
16
15
|
minRepoScoreThreshold: { type: "number" },
|
|
@@ -73,13 +72,9 @@ function formatArray(arr) {
|
|
|
73
72
|
/**
|
|
74
73
|
* Display current preferences in human-readable format.
|
|
75
74
|
*/
|
|
76
|
-
export function runConfigShow(
|
|
75
|
+
export function runConfigShow() {
|
|
77
76
|
const state = loadLocalState();
|
|
78
77
|
const prefs = state.preferences;
|
|
79
|
-
if (options.json) {
|
|
80
|
-
// JSON output handled by caller
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
78
|
console.log("\n⚙️ oss-scout preferences\n");
|
|
84
79
|
console.log(` githubUsername: ${prefs.githubUsername || "(not set)"}`);
|
|
85
80
|
console.log(` languages: ${formatArray(prefs.languages)}`);
|
|
@@ -89,7 +84,6 @@ export function runConfigShow(options) {
|
|
|
89
84
|
console.log(` maxIssueAgeDays: ${prefs.maxIssueAgeDays}`);
|
|
90
85
|
console.log(` minRepoScoreThreshold: ${prefs.minRepoScoreThreshold}`);
|
|
91
86
|
console.log(` includeDocIssues: ${prefs.includeDocIssues}`);
|
|
92
|
-
console.log(` preferredOrgs: ${formatArray(prefs.preferredOrgs)}`);
|
|
93
87
|
console.log(` projectCategories: ${formatArray(prefs.projectCategories)}`);
|
|
94
88
|
console.log(` excludeRepos: ${formatArray(prefs.excludeRepos)}`);
|
|
95
89
|
console.log(` excludeOrgs: ${formatArray(prefs.excludeOrgs)}`);
|
package/dist/commands/setup.js
CHANGED
|
@@ -59,11 +59,9 @@ export async function runSetup(options) {
|
|
|
59
59
|
const usernameInput = await ask(rl, usernamePrompt);
|
|
60
60
|
const githubUsername = usernameInput || usernameDefault;
|
|
61
61
|
// Languages
|
|
62
|
-
const defaultLangs = "
|
|
63
|
-
const langsInput = await ask(rl, `Preferred languages [${defaultLangs}]: `);
|
|
64
|
-
const languages = langsInput
|
|
65
|
-
? parseCSV(langsInput)
|
|
66
|
-
: ["typescript", "javascript"];
|
|
62
|
+
const defaultLangs = "any (all languages)";
|
|
63
|
+
const langsInput = await ask(rl, `Preferred languages (comma-separated, or "any" for all) [${defaultLangs}]: `);
|
|
64
|
+
const languages = langsInput ? parseCSV(langsInput) : ["any"];
|
|
67
65
|
// Issue labels
|
|
68
66
|
const defaultLabels = "good first issue, help wanted";
|
|
69
67
|
const labelsInput = await ask(rl, `Issue labels to search for [${defaultLabels}]: `);
|
|
@@ -79,9 +77,6 @@ export async function runSetup(options) {
|
|
|
79
77
|
// Minimum stars
|
|
80
78
|
const minStarsInput = await ask(rl, "Minimum repo stars [50]: ");
|
|
81
79
|
const minStars = minStarsInput ? parseInt(minStarsInput, 10) : 50;
|
|
82
|
-
// Preferred organizations
|
|
83
|
-
const orgsInput = await ask(rl, "Preferred organizations (comma-separated, optional): ");
|
|
84
|
-
const preferredOrgs = parseCSV(orgsInput);
|
|
85
80
|
// Project categories
|
|
86
81
|
const categoryOptions = ALL_CATEGORIES.join(", ");
|
|
87
82
|
const categoriesInput = await ask(rl, `Project categories (${categoryOptions}) [none]: `);
|
|
@@ -95,7 +90,6 @@ export async function runSetup(options) {
|
|
|
95
90
|
labels,
|
|
96
91
|
scope: scope.length > 0 ? scope : undefined,
|
|
97
92
|
excludeRepos,
|
|
98
|
-
preferredOrgs,
|
|
99
93
|
projectCategories,
|
|
100
94
|
minStars: isNaN(minStars) ? 50 : minStars,
|
|
101
95
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip command — manage the skip list for excluding issues from future searches.
|
|
3
|
+
*/
|
|
4
|
+
import type { SkippedIssue, ScoutState } from "../core/schemas.js";
|
|
5
|
+
/**
|
|
6
|
+
* Skip an issue by URL — adds it to the skip list and removes it from saved results.
|
|
7
|
+
* Tries to enrich metadata from saved results if available.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runSkip(options: {
|
|
10
|
+
issueUrl: string;
|
|
11
|
+
state?: ScoutState;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
skipped: boolean;
|
|
14
|
+
alreadySkipped: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* List all skipped issues.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runSkipList(options?: {
|
|
20
|
+
state?: ScoutState;
|
|
21
|
+
}): SkippedIssue[];
|
|
22
|
+
/**
|
|
23
|
+
* Clear all skipped issues.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runSkipClear(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Remove a specific issue from the skip list (unskip).
|
|
28
|
+
*/
|
|
29
|
+
export declare function runSkipRemove(options: {
|
|
30
|
+
issueUrl: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
removed: boolean;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip command — manage the skip list for excluding issues from future searches.
|
|
3
|
+
*/
|
|
4
|
+
import { loadLocalState, saveLocalState } from "../core/local-state.js";
|
|
5
|
+
import { createScout } from "../scout.js";
|
|
6
|
+
import { getGitHubToken } from "../core/utils.js";
|
|
7
|
+
/**
|
|
8
|
+
* Create an OssScout instance for skip operations.
|
|
9
|
+
* Uses gist persistence when a token is available, otherwise provided-state mode.
|
|
10
|
+
*/
|
|
11
|
+
async function createSkipScout(state) {
|
|
12
|
+
const token = getGitHubToken() ?? "";
|
|
13
|
+
if (token) {
|
|
14
|
+
return createScout({
|
|
15
|
+
githubToken: token,
|
|
16
|
+
persistence: "provided",
|
|
17
|
+
initialState: state,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return createScout({
|
|
21
|
+
githubToken: "",
|
|
22
|
+
persistence: "provided",
|
|
23
|
+
initialState: state,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Skip an issue by URL — adds it to the skip list and removes it from saved results.
|
|
28
|
+
* Tries to enrich metadata from saved results if available.
|
|
29
|
+
*/
|
|
30
|
+
export async function runSkip(options) {
|
|
31
|
+
const state = options.state ?? loadLocalState();
|
|
32
|
+
const scout = await createSkipScout(state);
|
|
33
|
+
const alreadySkipped = scout
|
|
34
|
+
.getSkippedIssues()
|
|
35
|
+
.some((s) => s.url === options.issueUrl);
|
|
36
|
+
if (alreadySkipped) {
|
|
37
|
+
return { skipped: false, alreadySkipped: true };
|
|
38
|
+
}
|
|
39
|
+
// Try to enrich metadata from saved results
|
|
40
|
+
const saved = scout
|
|
41
|
+
.getSavedResults()
|
|
42
|
+
.find((r) => r.issueUrl === options.issueUrl);
|
|
43
|
+
const metadata = saved
|
|
44
|
+
? { repo: saved.repo, number: saved.number, title: saved.title }
|
|
45
|
+
: parseIssueUrl(options.issueUrl);
|
|
46
|
+
scout.skipIssue(options.issueUrl, metadata);
|
|
47
|
+
saveLocalState(scout.getState());
|
|
48
|
+
await scout.checkpoint();
|
|
49
|
+
return { skipped: true, alreadySkipped: false };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List all skipped issues.
|
|
53
|
+
*/
|
|
54
|
+
export function runSkipList(options) {
|
|
55
|
+
const state = options?.state ?? loadLocalState();
|
|
56
|
+
return state.skippedIssues ?? [];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Clear all skipped issues.
|
|
60
|
+
*/
|
|
61
|
+
export async function runSkipClear() {
|
|
62
|
+
const state = loadLocalState();
|
|
63
|
+
const scout = await createSkipScout(state);
|
|
64
|
+
scout.clearSkippedIssues();
|
|
65
|
+
saveLocalState(scout.getState());
|
|
66
|
+
await scout.checkpoint();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Remove a specific issue from the skip list (unskip).
|
|
70
|
+
*/
|
|
71
|
+
export async function runSkipRemove(options) {
|
|
72
|
+
const state = loadLocalState();
|
|
73
|
+
const scout = await createSkipScout(state);
|
|
74
|
+
const before = scout.getSkippedIssues().length;
|
|
75
|
+
scout.unskipIssue(options.issueUrl);
|
|
76
|
+
const removed = before !== scout.getSkippedIssues().length;
|
|
77
|
+
saveLocalState(scout.getState());
|
|
78
|
+
await scout.checkpoint();
|
|
79
|
+
return { removed };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a GitHub issue URL to extract repo and number.
|
|
83
|
+
*/
|
|
84
|
+
function parseIssueUrl(url) {
|
|
85
|
+
const match = url.match(/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/);
|
|
86
|
+
if (!match)
|
|
87
|
+
return undefined;
|
|
88
|
+
return { repo: match[1], number: parseInt(match[2], 10), title: "" };
|
|
89
|
+
}
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getOctokit, checkRateLimit } from "./github.js";
|
|
6
6
|
import { debug, warn } from "./logger.js";
|
|
7
|
-
import { errorMessage } from "./errors.js";
|
|
7
|
+
import { ConfigurationError, errorMessage } from "./errors.js";
|
|
8
8
|
import { extractRepoFromUrl } from "./utils.js";
|
|
9
9
|
const MODULE = "bootstrap";
|
|
10
10
|
const STARRED_MAX_PAGES = 5;
|
|
@@ -13,7 +13,7 @@ const PER_PAGE = 100;
|
|
|
13
13
|
export async function bootstrapScout(scout, token) {
|
|
14
14
|
const username = scout.getPreferences().githubUsername;
|
|
15
15
|
if (!username) {
|
|
16
|
-
throw new
|
|
16
|
+
throw new ConfigurationError("GitHub username not configured. Run `oss-scout setup` first.");
|
|
17
17
|
}
|
|
18
18
|
const rateLimit = await checkRateLimit(token);
|
|
19
19
|
debug(MODULE, `Rate limit: ${rateLimit.remaining}/${rateLimit.limit}, resets at ${rateLimit.resetAt}`);
|
|
@@ -50,11 +50,16 @@ export class GistStateStore {
|
|
|
50
50
|
warn(MODULE, "No gist ID — cannot push");
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
|
+
const json = JSON.stringify(state, null, 2);
|
|
54
|
+
if (json.length > 900000) {
|
|
55
|
+
warn(MODULE, `State too large for gist (${Math.round(json.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
53
58
|
try {
|
|
54
59
|
await this.octokit.gists.update({
|
|
55
60
|
gist_id: this.gistId,
|
|
56
61
|
files: {
|
|
57
|
-
[GIST_FILENAME]: { content:
|
|
62
|
+
[GIST_FILENAME]: { content: json },
|
|
58
63
|
},
|
|
59
64
|
});
|
|
60
65
|
debug(MODULE, "State pushed to gist");
|
|
@@ -248,6 +253,7 @@ export function mergeStates(local, remote) {
|
|
|
248
253
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
249
254
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
250
255
|
savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
|
|
256
|
+
skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
|
|
251
257
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
252
258
|
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
|
|
253
259
|
new Date().toISOString(),
|
|
@@ -302,6 +308,18 @@ function mergeSavedResults(local, remote) {
|
|
|
302
308
|
}
|
|
303
309
|
return [...merged.values()];
|
|
304
310
|
}
|
|
311
|
+
function mergeSkippedIssues(local, remote) {
|
|
312
|
+
const merged = new Map();
|
|
313
|
+
for (const item of local)
|
|
314
|
+
merged.set(item.url, item);
|
|
315
|
+
for (const item of remote) {
|
|
316
|
+
const existing = merged.get(item.url);
|
|
317
|
+
if (!existing || item.skippedAt > existing.skippedAt) {
|
|
318
|
+
merged.set(item.url, item);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return [...merged.values()];
|
|
322
|
+
}
|
|
305
323
|
function pickFresherTimestamp(a, b) {
|
|
306
324
|
if (!a)
|
|
307
325
|
return b;
|
|
@@ -19,7 +19,6 @@ import { type ScoutStateReader } from "./issue-vetting.js";
|
|
|
19
19
|
*
|
|
20
20
|
* Search phases (in priority order):
|
|
21
21
|
* 0. Repos where user has merged PRs (highest merge probability)
|
|
22
|
-
* 0.5. Preferred organizations
|
|
23
22
|
* 1. Starred repos
|
|
24
23
|
* 2. General label-filtered search
|
|
25
24
|
* 3. Actively maintained repos
|
|
@@ -47,8 +46,8 @@ export declare class IssueDiscovery {
|
|
|
47
46
|
getStarredRepos(): string[];
|
|
48
47
|
/**
|
|
49
48
|
* Search for issues matching our criteria.
|
|
50
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
51
|
-
*
|
|
49
|
+
* Searches in priority order: merged-PR repos first (no label filter), then starred
|
|
50
|
+
* repos, then general search, then actively maintained repos.
|
|
52
51
|
* Filters out issues from low-scoring and excluded repos.
|
|
53
52
|
*
|
|
54
53
|
* @param options - Search configuration
|
|
@@ -74,6 +73,7 @@ export declare class IssueDiscovery {
|
|
|
74
73
|
labels?: string[];
|
|
75
74
|
maxResults?: number;
|
|
76
75
|
strategies?: SearchStrategy[];
|
|
76
|
+
skippedUrls?: Set<string>;
|
|
77
77
|
}): Promise<{
|
|
78
78
|
candidates: IssueCandidate[];
|
|
79
79
|
strategiesUsed: SearchStrategy[];
|
|
@@ -47,6 +47,8 @@ function buildIssueFilter(config) {
|
|
|
47
47
|
return false;
|
|
48
48
|
if (config.lowScoringRepos.has(repoFullName))
|
|
49
49
|
return false;
|
|
50
|
+
if (config.skippedUrls.has(item.html_url))
|
|
51
|
+
return false;
|
|
50
52
|
const updatedAt = new Date(item.updated_at);
|
|
51
53
|
const ageDays = daysBetween(updatedAt, config.now);
|
|
52
54
|
if (ageDays > config.maxAgeDays)
|
|
@@ -68,42 +70,6 @@ async function runPhase0(octokit, vetter, repos, baseQualifiers, maxResults, fil
|
|
|
68
70
|
rateLimitHit,
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
|
-
/** Phase 0.5: Search preferred organizations. */
|
|
72
|
-
async function runPhase05(octokit, vetter, orgsToSearch, baseQualifiers, labels, maxResults, phase0RepoSet, filterIssues) {
|
|
73
|
-
info(MODULE, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
|
|
74
|
-
const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
|
|
75
|
-
const orgOps = orgsToSearch.length - 1;
|
|
76
|
-
try {
|
|
77
|
-
const allItems = await searchWithChunkedLabels(octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`
|
|
78
|
-
.replace(/ +/g, " ")
|
|
79
|
-
.trim(), maxResults * 3);
|
|
80
|
-
if (allItems.length === 0) {
|
|
81
|
-
return { candidates: [], error: null, rateLimitHit: false };
|
|
82
|
-
}
|
|
83
|
-
const filtered = filterIssues(allItems).filter((item) => {
|
|
84
|
-
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
85
|
-
if (!repoFullName)
|
|
86
|
-
return false;
|
|
87
|
-
return !phase0RepoSet.has(repoFullName);
|
|
88
|
-
});
|
|
89
|
-
const { candidates, allFailed: allVetFailed, rateLimitHit, } = await vetter.vetIssuesParallel(filtered.slice(0, maxResults * 2).map((i) => i.html_url), maxResults, "preferred_org");
|
|
90
|
-
info(MODULE, `Found ${candidates.length} candidates from preferred orgs`);
|
|
91
|
-
return {
|
|
92
|
-
candidates,
|
|
93
|
-
error: allVetFailed ? "All preferred org issue vetting failed" : null,
|
|
94
|
-
rateLimitHit,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
const errMsg = errorMessage(error);
|
|
99
|
-
warn(MODULE, `Error searching preferred orgs: ${errMsg}`);
|
|
100
|
-
return {
|
|
101
|
-
candidates: [],
|
|
102
|
-
error: errMsg,
|
|
103
|
-
rateLimitHit: isRateLimitError(error),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
73
|
/** Phase 1: Search starred repos. */
|
|
108
74
|
async function runPhase1(octokit, vetter, repos, baseQualifiers, labels, maxResults, filterIssues) {
|
|
109
75
|
info(MODULE, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
@@ -228,7 +194,6 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
228
194
|
*
|
|
229
195
|
* Search phases (in priority order):
|
|
230
196
|
* 0. Repos where user has merged PRs (highest merge probability)
|
|
231
|
-
* 0.5. Preferred organizations
|
|
232
197
|
* 1. Starred repos
|
|
233
198
|
* 2. General label-filtered search
|
|
234
199
|
* 3. Actively maintained repos
|
|
@@ -264,8 +229,8 @@ export class IssueDiscovery {
|
|
|
264
229
|
}
|
|
265
230
|
/**
|
|
266
231
|
* Search for issues matching our criteria.
|
|
267
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
268
|
-
*
|
|
232
|
+
* Searches in priority order: merged-PR repos first (no label filter), then starred
|
|
233
|
+
* repos, then general search, then actively maintained repos.
|
|
269
234
|
* Filters out issues from low-scoring and excluded repos.
|
|
270
235
|
*
|
|
271
236
|
* @param options - Search configuration
|
|
@@ -329,6 +294,11 @@ export class IssueDiscovery {
|
|
|
329
294
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 60000).toISOString());
|
|
330
295
|
warn(MODULE, "Could not check rate limit — using conservative budget, skipping heavy phases:", errorMessage(error));
|
|
331
296
|
}
|
|
297
|
+
if (searchBudget <= 0) {
|
|
298
|
+
this.rateLimitWarning =
|
|
299
|
+
"GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
300
|
+
return { candidates: [], strategiesUsed: [] };
|
|
301
|
+
}
|
|
332
302
|
// Derive search context
|
|
333
303
|
const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
|
|
334
304
|
const starredRepos = this.getStarredRepos();
|
|
@@ -352,6 +322,7 @@ export class IssueDiscovery {
|
|
|
352
322
|
excludeOrgs: new Set((config.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
353
323
|
aiBlocklisted,
|
|
354
324
|
lowScoringRepos,
|
|
325
|
+
skippedUrls: options.skippedUrls ?? new Set(),
|
|
355
326
|
maxAgeDays: config.maxIssueAgeDays || 90,
|
|
356
327
|
now: new Date(),
|
|
357
328
|
includeDocIssues: config.includeDocIssues ?? true,
|
|
@@ -370,28 +341,6 @@ export class IssueDiscovery {
|
|
|
370
341
|
}
|
|
371
342
|
strategiesUsed.push("merged");
|
|
372
343
|
}
|
|
373
|
-
// Phase 0.5: Preferred organizations
|
|
374
|
-
const preferredOrgs = config.preferredOrgs ?? [];
|
|
375
|
-
if (allCandidates.length < maxResults &&
|
|
376
|
-
preferredOrgs.length > 0 &&
|
|
377
|
-
searchBudget >= CRITICAL_BUDGET_THRESHOLD &&
|
|
378
|
-
enabledStrategies.has("orgs")) {
|
|
379
|
-
if (phase0Repos.length > 0)
|
|
380
|
-
await sleep(INTER_PHASE_DELAY_MS);
|
|
381
|
-
const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
|
|
382
|
-
const orgsToSearch = preferredOrgs
|
|
383
|
-
.filter((org) => !phase0Orgs.has(org.toLowerCase()))
|
|
384
|
-
.slice(0, 5);
|
|
385
|
-
if (orgsToSearch.length > 0) {
|
|
386
|
-
const remaining = maxResults - allCandidates.length;
|
|
387
|
-
const result = await runPhase05(this.octokit, this.vetter, orgsToSearch, baseQualifiers, labels, remaining, phase0RepoSet, filterIssues);
|
|
388
|
-
allCandidates.push(...result.candidates);
|
|
389
|
-
phaseErrors["0.5"] = result.error;
|
|
390
|
-
if (result.rateLimitHit)
|
|
391
|
-
rateLimitHitDuringSearch = true;
|
|
392
|
-
}
|
|
393
|
-
strategiesUsed.push("orgs");
|
|
394
|
-
}
|
|
395
344
|
// Phase 1: Starred repos
|
|
396
345
|
if (allCandidates.length < maxResults &&
|
|
397
346
|
starredRepos.length > 0 &&
|
|
@@ -451,9 +400,6 @@ export class IssueDiscovery {
|
|
|
451
400
|
phaseErrors["0"]
|
|
452
401
|
? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}`
|
|
453
402
|
: null,
|
|
454
|
-
phaseErrors["0.5"]
|
|
455
|
-
? `Phase 0.5 (preferred orgs): ${phaseErrors["0.5"]}`
|
|
456
|
-
: null,
|
|
457
403
|
phaseErrors["1"]
|
|
458
404
|
? `Phase 1 (starred repos): ${phaseErrors["1"]}`
|
|
459
405
|
: null,
|
|
@@ -482,9 +428,8 @@ export class IssueDiscovery {
|
|
|
482
428
|
allCandidates.sort((a, b) => {
|
|
483
429
|
const priorityOrder = {
|
|
484
430
|
merged_pr: 0,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
normal: 3,
|
|
431
|
+
starred: 1,
|
|
432
|
+
normal: 2,
|
|
488
433
|
};
|
|
489
434
|
const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
|
|
490
435
|
if (priorityDiff !== 0)
|
|
@@ -17,8 +17,6 @@ export interface ScoutStateReader {
|
|
|
17
17
|
getReposWithMergedPRs(): string[];
|
|
18
18
|
/** User's starred repos (from GitHub). */
|
|
19
19
|
getStarredRepos(): string[];
|
|
20
|
-
/** Preferred GitHub orgs from user preferences. */
|
|
21
|
-
getPreferredOrgs(): string[];
|
|
22
20
|
/** Preferred project categories from user preferences. */
|
|
23
21
|
getProjectCategories(): ProjectCategory[];
|
|
24
22
|
/** Numeric quality score for a repo, or null if not evaluated. */
|
|
@@ -205,14 +205,10 @@ export class IssueVetter {
|
|
|
205
205
|
matchesPreferredCategory: matchesCategory,
|
|
206
206
|
});
|
|
207
207
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
208
|
-
const preferredOrgs = this.stateReader.getPreferredOrgs();
|
|
209
208
|
let searchPriority = "normal";
|
|
210
209
|
if (effectiveMergedCount > 0) {
|
|
211
210
|
searchPriority = "merged_pr";
|
|
212
211
|
}
|
|
213
|
-
else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
|
|
214
|
-
searchPriority = "preferred_org";
|
|
215
|
-
}
|
|
216
212
|
else if (starredRepos.includes(repoFullName)) {
|
|
217
213
|
searchPriority = "starred";
|
|
218
214
|
}
|
package/dist/core/local-state.js
CHANGED
|
@@ -39,8 +39,8 @@ export function loadLocalState() {
|
|
|
39
39
|
fs.copyFileSync(statePath, backupPath);
|
|
40
40
|
warn(MODULE, `Corrupt state backed up to ${backupPath}`);
|
|
41
41
|
}
|
|
42
|
-
catch {
|
|
43
|
-
|
|
42
|
+
catch (backupErr) {
|
|
43
|
+
warn(MODULE, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
44
44
|
}
|
|
45
45
|
return ScoutStateSchema.parse({ version: 1 });
|
|
46
46
|
}
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -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>;
|