@oss-scout/mcp 0.8.0 → 0.10.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/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +35 -14
- package/dist/mcp-server.bundle.cjs +1412 -662
- package/dist/tools.js +96 -109
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
"use strict";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -28888,13 +28889,13 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
28888
28889
|
}, true) ?? parseAnyDef(refs)
|
|
28889
28890
|
}), {}) : void 0;
|
|
28890
28891
|
const name = typeof options === "string" ? options : options?.nameStrategy === "title" ? void 0 : options?.name;
|
|
28891
|
-
const
|
|
28892
|
+
const main = parseDef(schema._def, name === void 0 ? refs : {
|
|
28892
28893
|
...refs,
|
|
28893
28894
|
currentPath: [...refs.basePath, refs.definitionPath, name]
|
|
28894
28895
|
}, false) ?? parseAnyDef(refs);
|
|
28895
28896
|
const title = typeof options === "object" && options.name !== void 0 && options.nameStrategy === "title" ? options.name : void 0;
|
|
28896
28897
|
if (title !== void 0) {
|
|
28897
|
-
|
|
28898
|
+
main.title = title;
|
|
28898
28899
|
}
|
|
28899
28900
|
if (refs.flags.hasReferencedOpenAiAnyType) {
|
|
28900
28901
|
if (!definitions) {
|
|
@@ -28915,9 +28916,9 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
28915
28916
|
}
|
|
28916
28917
|
}
|
|
28917
28918
|
const combined = name === void 0 ? definitions ? {
|
|
28918
|
-
...
|
|
28919
|
+
...main,
|
|
28919
28920
|
[refs.definitionPath]: definitions
|
|
28920
|
-
} :
|
|
28921
|
+
} : main : {
|
|
28921
28922
|
$ref: [
|
|
28922
28923
|
...refs.$refStrategy === "relative" ? [] : refs.basePath,
|
|
28923
28924
|
refs.definitionPath,
|
|
@@ -28925,7 +28926,7 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
28925
28926
|
].join("/"),
|
|
28926
28927
|
[refs.definitionPath]: {
|
|
28927
28928
|
...definitions,
|
|
28928
|
-
[name]:
|
|
28929
|
+
[name]: main
|
|
28929
28930
|
}
|
|
28930
28931
|
};
|
|
28931
28932
|
if (refs.target === "jsonSchema7") {
|
|
@@ -30262,8 +30263,8 @@ var Server = class extends Protocol {
|
|
|
30262
30263
|
this._loggingLevels = /* @__PURE__ */ new Map();
|
|
30263
30264
|
this.LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index]));
|
|
30264
30265
|
this.isMessageIgnored = (level, sessionId) => {
|
|
30265
|
-
const
|
|
30266
|
-
return
|
|
30266
|
+
const currentLevel2 = this._loggingLevels.get(sessionId);
|
|
30267
|
+
return currentLevel2 ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel2) : false;
|
|
30267
30268
|
};
|
|
30268
30269
|
this._capabilities = options?.capabilities ?? {};
|
|
30269
30270
|
this._instructions = options?.instructions;
|
|
@@ -35449,18 +35450,34 @@ throttling.VERSION = VERSION9;
|
|
|
35449
35450
|
throttling.triggersNotification = triggersNotification;
|
|
35450
35451
|
|
|
35451
35452
|
// ../core/dist/core/logger.js
|
|
35452
|
-
var
|
|
35453
|
+
var LEVEL_RANK = {
|
|
35454
|
+
silent: 0,
|
|
35455
|
+
warn: 1,
|
|
35456
|
+
info: 2,
|
|
35457
|
+
debug: 3
|
|
35458
|
+
};
|
|
35459
|
+
var currentLevel = "info";
|
|
35460
|
+
function setLogLevel(level) {
|
|
35461
|
+
currentLevel = level;
|
|
35462
|
+
}
|
|
35463
|
+
function shouldLog(level) {
|
|
35464
|
+
return LEVEL_RANK[currentLevel] >= LEVEL_RANK[level];
|
|
35465
|
+
}
|
|
35453
35466
|
function debug(module2, message, ...args) {
|
|
35454
|
-
if (!
|
|
35467
|
+
if (!shouldLog("debug"))
|
|
35455
35468
|
return;
|
|
35456
35469
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
35457
35470
|
console.error(`[${timestamp}] [DEBUG] [${module2}] ${message}`, ...args);
|
|
35458
35471
|
}
|
|
35459
35472
|
function info(module2, message, ...args) {
|
|
35473
|
+
if (!shouldLog("info"))
|
|
35474
|
+
return;
|
|
35460
35475
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
35461
35476
|
console.error(`[${timestamp}] [INFO] [${module2}] ${message}`, ...args);
|
|
35462
35477
|
}
|
|
35463
35478
|
function warn(module2, message, ...args) {
|
|
35479
|
+
if (!shouldLog("warn"))
|
|
35480
|
+
return;
|
|
35464
35481
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
35465
35482
|
console.error(`[${timestamp}] [WARN] [${module2}] ${message}`, ...args);
|
|
35466
35483
|
}
|
|
@@ -35556,10 +35573,15 @@ function isRateLimitError(error48) {
|
|
|
35556
35573
|
return true;
|
|
35557
35574
|
if (status === 403) {
|
|
35558
35575
|
const msg = errorMessage(error48).toLowerCase();
|
|
35559
|
-
return msg.includes("rate limit");
|
|
35576
|
+
return msg.includes("rate limit") || msg.includes("abuse detection");
|
|
35560
35577
|
}
|
|
35561
35578
|
return false;
|
|
35562
35579
|
}
|
|
35580
|
+
function rethrowIfFatal(error48) {
|
|
35581
|
+
if (getHttpStatusCode(error48) === 401 || isRateLimitError(error48)) {
|
|
35582
|
+
throw error48;
|
|
35583
|
+
}
|
|
35584
|
+
}
|
|
35563
35585
|
|
|
35564
35586
|
// ../core/dist/core/utils.js
|
|
35565
35587
|
function sleep(ms) {
|
|
@@ -35583,12 +35605,23 @@ function getCacheDir() {
|
|
|
35583
35605
|
return dir;
|
|
35584
35606
|
}
|
|
35585
35607
|
function extractRepoFromUrl(url2) {
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
|
|
35608
|
+
let parsed;
|
|
35609
|
+
try {
|
|
35610
|
+
parsed = new URL(url2);
|
|
35611
|
+
} catch {
|
|
35612
|
+
return null;
|
|
35613
|
+
}
|
|
35614
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
35615
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
35616
|
+
if (host === "api.github.com") {
|
|
35617
|
+
if (segments[0] === "repos" && segments.length >= 3) {
|
|
35618
|
+
return `${segments[1]}/${segments[2]}`;
|
|
35619
|
+
}
|
|
35620
|
+
return null;
|
|
35621
|
+
}
|
|
35622
|
+
if (host === "github.com" && segments.length >= 2) {
|
|
35623
|
+
return `${segments[0]}/${segments[1]}`;
|
|
35624
|
+
}
|
|
35592
35625
|
return null;
|
|
35593
35626
|
}
|
|
35594
35627
|
var OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -35597,25 +35630,29 @@ function isValidOwnerRepo(owner, repo) {
|
|
|
35597
35630
|
return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
|
|
35598
35631
|
}
|
|
35599
35632
|
function parseGitHubUrl(url2) {
|
|
35600
|
-
|
|
35633
|
+
const normalized = /^(?:www\.)?github\.com\//i.test(url2) ? `https://${url2}` : url2;
|
|
35634
|
+
let parsed;
|
|
35635
|
+
try {
|
|
35636
|
+
parsed = new URL(normalized);
|
|
35637
|
+
} catch {
|
|
35601
35638
|
return null;
|
|
35602
|
-
const prMatch = url2.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
35603
|
-
if (prMatch) {
|
|
35604
|
-
const owner = prMatch[1];
|
|
35605
|
-
const repo = prMatch[2];
|
|
35606
|
-
if (!isValidOwnerRepo(owner, repo))
|
|
35607
|
-
return null;
|
|
35608
|
-
return { owner, repo, number: parseInt(prMatch[3], 10), type: "pull" };
|
|
35609
|
-
}
|
|
35610
|
-
const issueMatch = url2.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
35611
|
-
if (issueMatch) {
|
|
35612
|
-
const owner = issueMatch[1];
|
|
35613
|
-
const repo = issueMatch[2];
|
|
35614
|
-
if (!isValidOwnerRepo(owner, repo))
|
|
35615
|
-
return null;
|
|
35616
|
-
return { owner, repo, number: parseInt(issueMatch[3], 10), type: "issues" };
|
|
35617
35639
|
}
|
|
35618
|
-
|
|
35640
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
35641
|
+
return null;
|
|
35642
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
35643
|
+
if (host !== "github.com")
|
|
35644
|
+
return null;
|
|
35645
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
35646
|
+
if (segments.length !== 4)
|
|
35647
|
+
return null;
|
|
35648
|
+
const [owner, repo, type, num] = segments;
|
|
35649
|
+
if (type !== "pull" && type !== "issues")
|
|
35650
|
+
return null;
|
|
35651
|
+
if (!isValidOwnerRepo(owner, repo))
|
|
35652
|
+
return null;
|
|
35653
|
+
if (!/^\d+$/.test(num))
|
|
35654
|
+
return null;
|
|
35655
|
+
return { owner, repo, number: parseInt(num, 10), type };
|
|
35619
35656
|
}
|
|
35620
35657
|
function daysBetween(from, to = /* @__PURE__ */ new Date()) {
|
|
35621
35658
|
return Math.max(0, Math.floor((to.getTime() - from.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
@@ -35626,8 +35663,9 @@ function getGitHubToken() {
|
|
|
35626
35663
|
if (tokenFetchAttempted)
|
|
35627
35664
|
return null;
|
|
35628
35665
|
tokenFetchAttempted = true;
|
|
35629
|
-
|
|
35630
|
-
|
|
35666
|
+
const envToken = process.env.GITHUB_TOKEN?.trim();
|
|
35667
|
+
if (envToken) {
|
|
35668
|
+
cachedGitHubToken = envToken;
|
|
35631
35669
|
return cachedGitHubToken;
|
|
35632
35670
|
}
|
|
35633
35671
|
try {
|
|
@@ -35642,7 +35680,7 @@ function getGitHubToken() {
|
|
|
35642
35680
|
return cachedGitHubToken;
|
|
35643
35681
|
}
|
|
35644
35682
|
} catch (err) {
|
|
35645
|
-
debug(MODULE2,
|
|
35683
|
+
debug(MODULE2, `gh auth token failed: ${errorMessage(err)}`);
|
|
35646
35684
|
}
|
|
35647
35685
|
return null;
|
|
35648
35686
|
}
|
|
@@ -35662,6 +35700,8 @@ var SearchBudgetTracker = class {
|
|
|
35662
35700
|
resetAt = 0;
|
|
35663
35701
|
/** Total calls recorded since init (for diagnostics). */
|
|
35664
35702
|
totalCalls = 0;
|
|
35703
|
+
/** Calls made since the last known quota reset (external accounting). */
|
|
35704
|
+
callsSinceReset = 0;
|
|
35665
35705
|
/**
|
|
35666
35706
|
* Initialize with pre-flight rate limit data from GitHub.
|
|
35667
35707
|
*/
|
|
@@ -35670,6 +35710,7 @@ var SearchBudgetTracker = class {
|
|
|
35670
35710
|
this.resetAt = new Date(resetAt).getTime();
|
|
35671
35711
|
this.callTimestamps = [];
|
|
35672
35712
|
this.totalCalls = 0;
|
|
35713
|
+
this.callsSinceReset = 0;
|
|
35673
35714
|
debug(MODULE3, `Initialized: ${remaining} remaining, resets at ${new Date(this.resetAt).toLocaleTimeString()}`);
|
|
35674
35715
|
}
|
|
35675
35716
|
/**
|
|
@@ -35678,8 +35719,28 @@ var SearchBudgetTracker = class {
|
|
|
35678
35719
|
recordCall() {
|
|
35679
35720
|
this.callTimestamps.push(Date.now());
|
|
35680
35721
|
this.totalCalls++;
|
|
35722
|
+
this.callsSinceReset++;
|
|
35681
35723
|
this.pruneOldTimestamps();
|
|
35682
35724
|
}
|
|
35725
|
+
/**
|
|
35726
|
+
* Replenish the external budget once GitHub's quota window has reset.
|
|
35727
|
+
* Without this, the pre-flight remaining acted as a never-replenishing
|
|
35728
|
+
* run-lifetime total, throttling long multi-phase runs to ~1 call/min
|
|
35729
|
+
* even though GitHub fully resets every 60 seconds (#119).
|
|
35730
|
+
*/
|
|
35731
|
+
refreshExternalBudget() {
|
|
35732
|
+
if (this.resetAt <= 0)
|
|
35733
|
+
return;
|
|
35734
|
+
const now = Date.now();
|
|
35735
|
+
if (now < this.resetAt)
|
|
35736
|
+
return;
|
|
35737
|
+
this.knownRemaining = SEARCH_RATE_LIMIT;
|
|
35738
|
+
this.callsSinceReset = 0;
|
|
35739
|
+
while (this.resetAt <= now) {
|
|
35740
|
+
this.resetAt += SEARCH_WINDOW_MS;
|
|
35741
|
+
}
|
|
35742
|
+
debug(MODULE3, "Quota window reset; external search budget replenished");
|
|
35743
|
+
}
|
|
35683
35744
|
/**
|
|
35684
35745
|
* Remove timestamps older than the sliding window.
|
|
35685
35746
|
*/
|
|
@@ -35701,8 +35762,9 @@ var SearchBudgetTracker = class {
|
|
|
35701
35762
|
* and the pre-flight remaining quota from GitHub.
|
|
35702
35763
|
*/
|
|
35703
35764
|
getEffectiveBudget() {
|
|
35765
|
+
this.refreshExternalBudget();
|
|
35704
35766
|
const localBudget = EFFECTIVE_BUDGET - this.callTimestamps.length;
|
|
35705
|
-
const externalBudget = this.knownRemaining - this.
|
|
35767
|
+
const externalBudget = this.knownRemaining - this.callsSinceReset;
|
|
35706
35768
|
return Math.max(0, Math.min(localBudget, externalBudget));
|
|
35707
35769
|
}
|
|
35708
35770
|
/**
|
|
@@ -35725,6 +35787,12 @@ var SearchBudgetTracker = class {
|
|
|
35725
35787
|
}
|
|
35726
35788
|
const oldestInWindow = this.callTimestamps[0];
|
|
35727
35789
|
if (!oldestInWindow) {
|
|
35790
|
+
const untilReset = this.resetAt - Date.now();
|
|
35791
|
+
if (this.resetAt > 0 && untilReset > 0) {
|
|
35792
|
+
debug(MODULE3, `External quota exhausted, waiting ${untilReset}ms for reset`);
|
|
35793
|
+
await sleep(untilReset + 100);
|
|
35794
|
+
continue;
|
|
35795
|
+
}
|
|
35728
35796
|
return;
|
|
35729
35797
|
}
|
|
35730
35798
|
const waitUntil = oldestInWindow + SEARCH_WINDOW_MS;
|
|
@@ -35770,12 +35838,7 @@ var SCOPE_LABELS = {
|
|
|
35770
35838
|
};
|
|
35771
35839
|
|
|
35772
35840
|
// ../core/dist/core/schemas.js
|
|
35773
|
-
var IssueStatusSchema = external_exports3.enum([
|
|
35774
|
-
"candidate",
|
|
35775
|
-
"claimed",
|
|
35776
|
-
"in_progress",
|
|
35777
|
-
"pr_submitted"
|
|
35778
|
-
]);
|
|
35841
|
+
var IssueStatusSchema = external_exports3.enum(["candidate"]);
|
|
35779
35842
|
var ProjectCategorySchema = external_exports3.enum([
|
|
35780
35843
|
"nonprofit",
|
|
35781
35844
|
"devtools",
|
|
@@ -35802,12 +35865,15 @@ var CONCRETE_STRATEGIES = [
|
|
|
35802
35865
|
"broad",
|
|
35803
35866
|
"maintained"
|
|
35804
35867
|
];
|
|
35805
|
-
var RepoSignalsSchema = external_exports3.
|
|
35868
|
+
var RepoSignalsSchema = external_exports3.looseObject({
|
|
35806
35869
|
hasActiveMaintainers: external_exports3.boolean(),
|
|
35870
|
+
// Retained for backward compatibility but no longer affects the repo score
|
|
35871
|
+
// (#167): nothing computes it, and hasActiveMaintainers is the live activity
|
|
35872
|
+
// proxy. Kept so old persisted state and the search JSON output still parse.
|
|
35807
35873
|
isResponsive: external_exports3.boolean(),
|
|
35808
35874
|
hasHostileComments: external_exports3.boolean()
|
|
35809
35875
|
});
|
|
35810
|
-
var RepoScoreSchema = external_exports3.
|
|
35876
|
+
var RepoScoreSchema = external_exports3.looseObject({
|
|
35811
35877
|
repo: external_exports3.string(),
|
|
35812
35878
|
score: external_exports3.number(),
|
|
35813
35879
|
mergedPRCount: external_exports3.number(),
|
|
@@ -35819,17 +35885,17 @@ var RepoScoreSchema = external_exports3.object({
|
|
|
35819
35885
|
stargazersCount: external_exports3.number().optional(),
|
|
35820
35886
|
language: external_exports3.string().nullable().optional()
|
|
35821
35887
|
});
|
|
35822
|
-
var StoredMergedPRSchema = external_exports3.
|
|
35888
|
+
var StoredMergedPRSchema = external_exports3.looseObject({
|
|
35823
35889
|
url: external_exports3.string(),
|
|
35824
35890
|
title: external_exports3.string(),
|
|
35825
35891
|
mergedAt: external_exports3.string()
|
|
35826
35892
|
});
|
|
35827
|
-
var StoredClosedPRSchema = external_exports3.
|
|
35893
|
+
var StoredClosedPRSchema = external_exports3.looseObject({
|
|
35828
35894
|
url: external_exports3.string(),
|
|
35829
35895
|
title: external_exports3.string(),
|
|
35830
35896
|
closedAt: external_exports3.string()
|
|
35831
35897
|
});
|
|
35832
|
-
var StoredOpenPRSchema = external_exports3.
|
|
35898
|
+
var StoredOpenPRSchema = external_exports3.looseObject({
|
|
35833
35899
|
url: external_exports3.string(),
|
|
35834
35900
|
title: external_exports3.string(),
|
|
35835
35901
|
openedAt: external_exports3.string()
|
|
@@ -35889,7 +35955,7 @@ var TrackedIssueSchema = external_exports3.object({
|
|
|
35889
35955
|
vetted: external_exports3.boolean(),
|
|
35890
35956
|
vettingResult: IssueVettingResultSchema.optional()
|
|
35891
35957
|
});
|
|
35892
|
-
var SkippedIssueSchema = external_exports3.
|
|
35958
|
+
var SkippedIssueSchema = external_exports3.looseObject({
|
|
35893
35959
|
url: external_exports3.string(),
|
|
35894
35960
|
repo: external_exports3.string(),
|
|
35895
35961
|
number: external_exports3.number(),
|
|
@@ -35897,7 +35963,7 @@ var SkippedIssueSchema = external_exports3.object({
|
|
|
35897
35963
|
skippedAt: external_exports3.string()
|
|
35898
35964
|
});
|
|
35899
35965
|
var HorizonSchema = external_exports3.enum(["quick-win", "bigger-bet"]);
|
|
35900
|
-
var SavedCandidateSchema = external_exports3.
|
|
35966
|
+
var SavedCandidateSchema = external_exports3.looseObject({
|
|
35901
35967
|
issueUrl: external_exports3.string(),
|
|
35902
35968
|
repo: external_exports3.string(),
|
|
35903
35969
|
number: external_exports3.number(),
|
|
@@ -35905,14 +35971,23 @@ var SavedCandidateSchema = external_exports3.object({
|
|
|
35905
35971
|
labels: external_exports3.array(external_exports3.string()),
|
|
35906
35972
|
recommendation: external_exports3.enum(["approve", "skip", "needs_review"]),
|
|
35907
35973
|
viabilityScore: external_exports3.number(),
|
|
35908
|
-
|
|
35974
|
+
// Tightened from z.string() to the actual 3-value union (#158). `.catch`
|
|
35975
|
+
// keeps old persisted state loadable: any unrecognized stored value (or a
|
|
35976
|
+
// future-version value) decodes to "normal" instead of failing the parse.
|
|
35977
|
+
searchPriority: external_exports3.enum(["merged_pr", "starred", "normal"]).catch("normal"),
|
|
35909
35978
|
firstSeenAt: external_exports3.string(),
|
|
35910
35979
|
lastSeenAt: external_exports3.string(),
|
|
35911
35980
|
lastScore: external_exports3.number(),
|
|
35912
|
-
horizon: HorizonSchema.optional()
|
|
35981
|
+
horizon: HorizonSchema.optional(),
|
|
35982
|
+
/**
|
|
35983
|
+
* Availability status recorded by the previous `vet-list` run, so the next
|
|
35984
|
+
* run can report transitions ("was available, now claimed") instead of
|
|
35985
|
+
* re-diffing full snapshots (#165). "error" is never stored.
|
|
35986
|
+
*/
|
|
35987
|
+
lastStatus: external_exports3.enum(["still_available", "claimed", "closed", "has_pr"]).optional()
|
|
35913
35988
|
});
|
|
35914
35989
|
var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
|
|
35915
|
-
var ScoutPreferencesSchema = external_exports3.
|
|
35990
|
+
var ScoutPreferencesSchema = external_exports3.looseObject({
|
|
35916
35991
|
githubUsername: external_exports3.string().default(""),
|
|
35917
35992
|
languages: external_exports3.array(external_exports3.string()).default(["any"]),
|
|
35918
35993
|
labels: external_exports3.array(external_exports3.string()).default(["good first issue", "help wanted"]),
|
|
@@ -35928,8 +36003,28 @@ var ScoutPreferencesSchema = external_exports3.object({
|
|
|
35928
36003
|
interPhaseDelayMs: external_exports3.number().min(0).max(12e4).default(3e4),
|
|
35929
36004
|
persistence: PersistenceModeSchema.default("local"),
|
|
35930
36005
|
defaultStrategy: external_exports3.array(SearchStrategySchema).optional(),
|
|
36006
|
+
/**
|
|
36007
|
+
* Persisted personalization defaults (#168). The `--prefer-languages`,
|
|
36008
|
+
* `--prefer-repos`, and `--diversity-ratio` search flags override these when
|
|
36009
|
+
* passed, so a stored preference removes the need to retype the boost every
|
|
36010
|
+
* search. Empty / 0 disables the corresponding signal (same as the flags).
|
|
36011
|
+
*/
|
|
36012
|
+
preferLanguages: external_exports3.array(external_exports3.string()).default([]),
|
|
36013
|
+
preferRepos: external_exports3.array(external_exports3.string()).default([]),
|
|
36014
|
+
diversityRatio: external_exports3.number().min(0).max(1).default(0),
|
|
36015
|
+
// Soft penalty (milder than the hard excludeRepos filter): candidates in
|
|
36016
|
+
// these `owner/repo` slugs are pushed down the ranking but not removed (#168).
|
|
36017
|
+
avoidRepos: external_exports3.array(external_exports3.string()).default([]),
|
|
36018
|
+
// Soft boost for candidates whose issue labels match one of these types,
|
|
36019
|
+
// case-insensitive (e.g. "bug", "good first issue") (#168).
|
|
36020
|
+
boostIssueTypes: external_exports3.array(external_exports3.string()).default([]),
|
|
35931
36021
|
broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
|
|
35932
|
-
|
|
36022
|
+
/**
|
|
36023
|
+
* Skip the expensive broad phase once this many candidates were found by
|
|
36024
|
+
* the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
|
|
36025
|
+
* satisfiable; 0 disables skipping entirely.
|
|
36026
|
+
*/
|
|
36027
|
+
skipBroadWhenSufficientResults: external_exports3.number().int().min(0).max(100).default(8),
|
|
35933
36028
|
/**
|
|
35934
36029
|
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
35935
36030
|
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
@@ -35956,7 +36051,7 @@ var ScoutPreferencesSchema = external_exports3.object({
|
|
|
35956
36051
|
*/
|
|
35957
36052
|
featuresSplitRatio: external_exports3.number().min(0).max(1).default(0.6)
|
|
35958
36053
|
});
|
|
35959
|
-
var ScoutStateSchema = external_exports3.
|
|
36054
|
+
var ScoutStateSchema = external_exports3.looseObject({
|
|
35960
36055
|
version: external_exports3.literal(1),
|
|
35961
36056
|
preferences: ScoutPreferencesSchema.default(() => ScoutPreferencesSchema.parse({})),
|
|
35962
36057
|
repoScores: external_exports3.record(external_exports3.string(), RepoScoreSchema).default({}),
|
|
@@ -35967,10 +36062,30 @@ var ScoutStateSchema = external_exports3.object({
|
|
|
35967
36062
|
openPRs: external_exports3.array(StoredOpenPRSchema).default([]),
|
|
35968
36063
|
savedResults: external_exports3.array(SavedCandidateSchema).default([]),
|
|
35969
36064
|
skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
|
|
36065
|
+
/**
|
|
36066
|
+
* Deletion tombstones (#117). Without these, the gist union-merge would
|
|
36067
|
+
* resurrect any saved result or skipped issue removed on one machine the
|
|
36068
|
+
* next time it merged against another machine's copy that still had it.
|
|
36069
|
+
* A tombstone suppresses an item across a merge until the item is
|
|
36070
|
+
* re-added with a newer timestamp. Pruned after 90 days.
|
|
36071
|
+
*/
|
|
36072
|
+
tombstones: external_exports3.array(external_exports3.looseObject({
|
|
36073
|
+
url: external_exports3.string(),
|
|
36074
|
+
removedAt: external_exports3.string()
|
|
36075
|
+
})).default([]),
|
|
36076
|
+
/**
|
|
36077
|
+
* When preferences were last changed (#117). The gist merge previously
|
|
36078
|
+
* always took the remote preferences, silently reverting a local edit;
|
|
36079
|
+
* the side with the fresher timestamp now wins.
|
|
36080
|
+
*/
|
|
36081
|
+
preferencesUpdatedAt: external_exports3.string().optional(),
|
|
35970
36082
|
lastSearchAt: external_exports3.string().optional(),
|
|
35971
36083
|
lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
35972
36084
|
gistId: external_exports3.string().optional()
|
|
35973
36085
|
});
|
|
36086
|
+
function parseScoutState(raw) {
|
|
36087
|
+
return ScoutStateSchema.parse(raw);
|
|
36088
|
+
}
|
|
35974
36089
|
|
|
35975
36090
|
// ../core/dist/core/issue-filtering.js
|
|
35976
36091
|
var DOC_ONLY_LABELS = /* @__PURE__ */ new Set([
|
|
@@ -36268,6 +36383,10 @@ var fs2 = __toESM(require("fs"), 1);
|
|
|
36268
36383
|
var path2 = __toESM(require("path"), 1);
|
|
36269
36384
|
var crypto = __toESM(require("crypto"), 1);
|
|
36270
36385
|
var MODULE4 = "http-cache";
|
|
36386
|
+
var CACHE_SCHEMA_VERSION = "v1";
|
|
36387
|
+
function versionedCacheKey(key) {
|
|
36388
|
+
return `${CACHE_SCHEMA_VERSION}:${key}`;
|
|
36389
|
+
}
|
|
36271
36390
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
36272
36391
|
var HttpCache = class {
|
|
36273
36392
|
cacheDir;
|
|
@@ -36290,13 +36409,21 @@ var HttpCache = class {
|
|
|
36290
36409
|
* (e.g., caching aggregated results from paginated API calls).
|
|
36291
36410
|
*/
|
|
36292
36411
|
getIfFresh(key, maxAgeMs) {
|
|
36412
|
+
return this.getEntryIfFresh(key, maxAgeMs)?.body ?? null;
|
|
36413
|
+
}
|
|
36414
|
+
/**
|
|
36415
|
+
* Like {@link getIfFresh}, but returns the whole entry so callers can
|
|
36416
|
+
* distinguish "no fresh entry" (null) from a legitimately cached falsy
|
|
36417
|
+
* body (`0`, `""`, `false`, `null`).
|
|
36418
|
+
*/
|
|
36419
|
+
getEntryIfFresh(key, maxAgeMs) {
|
|
36293
36420
|
const entry = this.get(key);
|
|
36294
36421
|
if (!entry)
|
|
36295
36422
|
return null;
|
|
36296
36423
|
const age = Date.now() - new Date(entry.cachedAt).getTime();
|
|
36297
36424
|
if (!Number.isFinite(age) || age < 0 || age > maxAgeMs)
|
|
36298
36425
|
return null;
|
|
36299
|
-
return entry
|
|
36426
|
+
return entry;
|
|
36300
36427
|
}
|
|
36301
36428
|
/**
|
|
36302
36429
|
* Look up a cached response. Returns `null` if no cache entry exists.
|
|
@@ -36447,12 +36574,21 @@ function getHttpCache() {
|
|
|
36447
36574
|
}
|
|
36448
36575
|
return _httpCache;
|
|
36449
36576
|
}
|
|
36450
|
-
async function
|
|
36451
|
-
const existing = cache.getInflight(
|
|
36577
|
+
async function withInflightDedup(cache, key, fn) {
|
|
36578
|
+
const existing = cache.getInflight(key);
|
|
36452
36579
|
if (existing) {
|
|
36453
|
-
debug(MODULE4, `Dedup hit for ${
|
|
36580
|
+
debug(MODULE4, `Dedup hit for ${key}`);
|
|
36454
36581
|
return await existing;
|
|
36455
36582
|
}
|
|
36583
|
+
const promise2 = fn();
|
|
36584
|
+
const cleanup = cache.setInflight(key, promise2);
|
|
36585
|
+
try {
|
|
36586
|
+
return await promise2;
|
|
36587
|
+
} finally {
|
|
36588
|
+
cleanup();
|
|
36589
|
+
}
|
|
36590
|
+
}
|
|
36591
|
+
async function cachedRequest(cache, url2, fetcher) {
|
|
36456
36592
|
const doFetch = async () => {
|
|
36457
36593
|
const extraHeaders = {};
|
|
36458
36594
|
const cached2 = cache.get(url2);
|
|
@@ -36473,28 +36609,34 @@ async function cachedRequest(cache, url2, fetcher) {
|
|
|
36473
36609
|
debug(MODULE4, `304 cache hit for ${url2}`);
|
|
36474
36610
|
return freshCached.body;
|
|
36475
36611
|
}
|
|
36612
|
+
debug(MODULE4, `304 but cache entry vanished for ${url2}, refetching`);
|
|
36613
|
+
const response = await fetcher({});
|
|
36614
|
+
const etag = response.headers?.["etag"];
|
|
36615
|
+
if (etag) {
|
|
36616
|
+
cache.set(url2, etag, response.data);
|
|
36617
|
+
}
|
|
36618
|
+
return response.data;
|
|
36476
36619
|
}
|
|
36477
36620
|
throw err;
|
|
36478
36621
|
}
|
|
36479
36622
|
};
|
|
36480
|
-
|
|
36481
|
-
const cleanup = cache.setInflight(url2, promise2);
|
|
36482
|
-
try {
|
|
36483
|
-
const result = await promise2;
|
|
36484
|
-
return result;
|
|
36485
|
-
} finally {
|
|
36486
|
-
cleanup();
|
|
36487
|
-
}
|
|
36623
|
+
return withInflightDedup(cache, url2, doFetch);
|
|
36488
36624
|
}
|
|
36489
36625
|
async function cachedTimeBased(cache, key, maxAgeMs, fetcher) {
|
|
36490
|
-
const cached2 = cache.
|
|
36626
|
+
const cached2 = cache.getEntryIfFresh(key, maxAgeMs);
|
|
36491
36627
|
if (cached2) {
|
|
36492
36628
|
debug(MODULE4, `Time-based cache hit for ${key}`);
|
|
36493
|
-
return cached2;
|
|
36629
|
+
return cached2.body;
|
|
36494
36630
|
}
|
|
36495
|
-
|
|
36496
|
-
|
|
36497
|
-
|
|
36631
|
+
return withInflightDedup(cache, key, async () => {
|
|
36632
|
+
const fresh = cache.getEntryIfFresh(key, maxAgeMs);
|
|
36633
|
+
if (fresh) {
|
|
36634
|
+
return fresh.body;
|
|
36635
|
+
}
|
|
36636
|
+
const result = await fetcher();
|
|
36637
|
+
cache.set(key, "", result);
|
|
36638
|
+
return result;
|
|
36639
|
+
});
|
|
36498
36640
|
}
|
|
36499
36641
|
function isNotModifiedError(err) {
|
|
36500
36642
|
return getHttpStatusCode(err) === 304;
|
|
@@ -36531,23 +36673,36 @@ function buildLinkedPRFromTimelineEvent(e, context) {
|
|
|
36531
36673
|
};
|
|
36532
36674
|
}
|
|
36533
36675
|
var MODULE5 = "issue-eligibility";
|
|
36534
|
-
var
|
|
36535
|
-
|
|
36536
|
-
|
|
36537
|
-
|
|
36538
|
-
|
|
36539
|
-
|
|
36540
|
-
|
|
36541
|
-
|
|
36542
|
-
|
|
36543
|
-
|
|
36544
|
-
|
|
36545
|
-
"i'm on it",
|
|
36546
|
-
"i'll submit a pr",
|
|
36547
|
-
"i will submit a pr",
|
|
36548
|
-
"working on a fix",
|
|
36549
|
-
"working on a pr"
|
|
36676
|
+
var ISSUE_OBJECT = String.raw`(?:this\b|it\b|that\b|the\b|#\d+|issue\b)`;
|
|
36677
|
+
var FIRST_PERSON_CLAIM_PATTERNS = [
|
|
36678
|
+
new RegExp(String.raw`\bi\s*(?:'ll|will) take ${ISSUE_OBJECT}`),
|
|
36679
|
+
new RegExp(String.raw`\bi\s*(?:'d|would) (?:like|love) to work on ${ISSUE_OBJECT}`),
|
|
36680
|
+
/\bi\s*(?:'m|am) on it\b/,
|
|
36681
|
+
/\bi\s*(?:'ll|will) submit a pr\b/,
|
|
36682
|
+
/\bassigned to me\b/
|
|
36683
|
+
];
|
|
36684
|
+
var GENERIC_WORKING_PATTERNS = [
|
|
36685
|
+
/\bworking on (?:this|it)\b/,
|
|
36686
|
+
/\bworking on a (?:fix|pr)\b/
|
|
36550
36687
|
];
|
|
36688
|
+
var PERMISSION_CLAIM_PATTERN = new RegExp(String.raw`\b(?:can|may|could) i (?:work on|take) ${ISSUE_OBJECT}`);
|
|
36689
|
+
var NON_CLAIM_SUBJECTS = /\b(?:anyone|anybody|someone|somebody|who|whoever|nobody|no[- ]?one|not)\b/;
|
|
36690
|
+
function commentClaimsIssue(body) {
|
|
36691
|
+
const clauses = body.toLowerCase().split(/(?<=[.!?])|\n+/);
|
|
36692
|
+
for (const clause of clauses) {
|
|
36693
|
+
if (PERMISSION_CLAIM_PATTERN.test(clause))
|
|
36694
|
+
return true;
|
|
36695
|
+
if (clause.trimEnd().endsWith("?"))
|
|
36696
|
+
continue;
|
|
36697
|
+
if (FIRST_PERSON_CLAIM_PATTERNS.some((p) => p.test(clause)))
|
|
36698
|
+
return true;
|
|
36699
|
+
if (NON_CLAIM_SUBJECTS.test(clause))
|
|
36700
|
+
continue;
|
|
36701
|
+
if (GENERIC_WORKING_PATTERNS.some((p) => p.test(clause)))
|
|
36702
|
+
return true;
|
|
36703
|
+
}
|
|
36704
|
+
return false;
|
|
36705
|
+
}
|
|
36551
36706
|
async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
36552
36707
|
try {
|
|
36553
36708
|
const timeline = await paginateAll((page) => octokit.issues.listEventsForTimeline({
|
|
@@ -36572,67 +36727,68 @@ async function checkNoExistingPR(octokit, owner, repo, issueNumber) {
|
|
|
36572
36727
|
}
|
|
36573
36728
|
return { passed: linkedPRCount === 0, linkedPR };
|
|
36574
36729
|
} catch (error48) {
|
|
36575
|
-
|
|
36576
|
-
throw error48;
|
|
36577
|
-
}
|
|
36730
|
+
rethrowIfFatal(error48);
|
|
36578
36731
|
const errMsg = errorMessage(error48);
|
|
36579
36732
|
warn(MODULE5, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
36580
36733
|
return { passed: true, inconclusive: true, reason: errMsg, linkedPR: null };
|
|
36581
36734
|
}
|
|
36582
36735
|
}
|
|
36583
36736
|
var MERGED_PR_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
36584
|
-
async function checkUserMergedPRsInRepo(octokit, owner, repo) {
|
|
36737
|
+
async function checkUserMergedPRsInRepo(octokit, owner, repo, tracker = getSearchBudgetTracker()) {
|
|
36585
36738
|
const cache = getHttpCache();
|
|
36586
|
-
const cacheKey = `merged-prs:${owner}/${repo}
|
|
36587
|
-
|
|
36588
|
-
|
|
36589
|
-
|
|
36590
|
-
|
|
36591
|
-
try {
|
|
36592
|
-
const tracker = getSearchBudgetTracker();
|
|
36593
|
-
await tracker.waitForBudget();
|
|
36594
|
-
try {
|
|
36595
|
-
const { data } = await octokit.search.issuesAndPullRequests({
|
|
36596
|
-
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
36597
|
-
per_page: 1
|
|
36598
|
-
// We only need total_count
|
|
36599
|
-
});
|
|
36600
|
-
cache.set(cacheKey, "", data.total_count);
|
|
36601
|
-
return data.total_count;
|
|
36602
|
-
} finally {
|
|
36603
|
-
tracker.recordCall();
|
|
36739
|
+
const cacheKey = versionedCacheKey(`merged-prs:${owner}/${repo}`);
|
|
36740
|
+
return withInflightDedup(cache, cacheKey, async () => {
|
|
36741
|
+
const cached2 = cache.getIfFresh(cacheKey, MERGED_PR_CACHE_TTL_MS);
|
|
36742
|
+
if (cached2 != null && typeof cached2 === "number") {
|
|
36743
|
+
return cached2;
|
|
36604
36744
|
}
|
|
36605
|
-
|
|
36606
|
-
|
|
36607
|
-
|
|
36745
|
+
try {
|
|
36746
|
+
await tracker.waitForBudget();
|
|
36747
|
+
try {
|
|
36748
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
36749
|
+
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
36750
|
+
per_page: 1
|
|
36751
|
+
// We only need total_count
|
|
36752
|
+
});
|
|
36753
|
+
cache.set(cacheKey, "", data.total_count);
|
|
36754
|
+
return data.total_count;
|
|
36755
|
+
} finally {
|
|
36756
|
+
tracker.recordCall();
|
|
36757
|
+
}
|
|
36758
|
+
} catch (error48) {
|
|
36759
|
+
rethrowIfFatal(error48);
|
|
36760
|
+
const errMsg = errorMessage(error48);
|
|
36761
|
+
warn(MODULE5, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Treating as unknown.`);
|
|
36762
|
+
return null;
|
|
36608
36763
|
}
|
|
36609
|
-
|
|
36610
|
-
warn(MODULE5, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
36611
|
-
return 0;
|
|
36612
|
-
}
|
|
36764
|
+
});
|
|
36613
36765
|
}
|
|
36614
36766
|
async function checkNotClaimed(octokit, owner, repo, issueNumber, commentCount) {
|
|
36615
36767
|
if (commentCount === 0)
|
|
36616
36768
|
return { passed: true };
|
|
36617
36769
|
try {
|
|
36618
|
-
const
|
|
36619
|
-
|
|
36620
|
-
|
|
36621
|
-
|
|
36622
|
-
|
|
36623
|
-
|
|
36624
|
-
|
|
36770
|
+
const PER_PAGE = 100;
|
|
36771
|
+
const lastPage = Math.max(1, Math.ceil(commentCount / PER_PAGE));
|
|
36772
|
+
const pagesToFetch = lastPage > 1 ? [lastPage - 1, lastPage] : [1];
|
|
36773
|
+
const recentComments = [];
|
|
36774
|
+
for (const page of pagesToFetch) {
|
|
36775
|
+
const response = await octokit.issues.listComments({
|
|
36776
|
+
owner,
|
|
36777
|
+
repo,
|
|
36778
|
+
issue_number: issueNumber,
|
|
36779
|
+
per_page: PER_PAGE,
|
|
36780
|
+
page
|
|
36781
|
+
});
|
|
36782
|
+
recentComments.push(...response.data);
|
|
36783
|
+
}
|
|
36625
36784
|
for (const comment of recentComments) {
|
|
36626
|
-
|
|
36627
|
-
if (CLAIM_PHRASES.some((phrase) => body.includes(phrase))) {
|
|
36785
|
+
if (commentClaimsIssue(comment.body || "")) {
|
|
36628
36786
|
return { passed: false };
|
|
36629
36787
|
}
|
|
36630
36788
|
}
|
|
36631
36789
|
return { passed: true };
|
|
36632
36790
|
} catch (error48) {
|
|
36633
|
-
|
|
36634
|
-
throw error48;
|
|
36635
|
-
}
|
|
36791
|
+
rethrowIfFatal(error48);
|
|
36636
36792
|
const errMsg = errorMessage(error48);
|
|
36637
36793
|
warn(MODULE5, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
36638
36794
|
return { passed: true, inconclusive: true, reason: errMsg };
|
|
@@ -36653,8 +36809,30 @@ function analyzeRequirements(body) {
|
|
|
36653
36809
|
return indicators.filter(Boolean).length >= 2;
|
|
36654
36810
|
}
|
|
36655
36811
|
|
|
36812
|
+
// ../core/dist/core/probe-repo-file.js
|
|
36813
|
+
var MODULE6 = "probe-repo-file";
|
|
36814
|
+
async function probeRepoFile(octokit, owner, repo, path5) {
|
|
36815
|
+
try {
|
|
36816
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
36817
|
+
if (data && typeof data === "object" && "content" in data && typeof data.content === "string") {
|
|
36818
|
+
return {
|
|
36819
|
+
text: Buffer.from(data.content, "base64").toString("utf-8"),
|
|
36820
|
+
transient: false
|
|
36821
|
+
};
|
|
36822
|
+
}
|
|
36823
|
+
return { text: null, transient: false };
|
|
36824
|
+
} catch (error48) {
|
|
36825
|
+
const status = getHttpStatusCode(error48);
|
|
36826
|
+
if (status === 404)
|
|
36827
|
+
return { text: null, transient: false };
|
|
36828
|
+
rethrowIfFatal(error48);
|
|
36829
|
+
warn(MODULE6, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
|
|
36830
|
+
return { text: null, transient: true };
|
|
36831
|
+
}
|
|
36832
|
+
}
|
|
36833
|
+
|
|
36656
36834
|
// ../core/dist/core/repo-health.js
|
|
36657
|
-
var
|
|
36835
|
+
var MODULE7 = "repo-health";
|
|
36658
36836
|
var guidelinesCache = /* @__PURE__ */ new Map();
|
|
36659
36837
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
36660
36838
|
var HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
@@ -36705,19 +36883,11 @@ async function checkProjectHealth(octokit, owner, repo) {
|
|
|
36705
36883
|
};
|
|
36706
36884
|
});
|
|
36707
36885
|
} catch (error48) {
|
|
36708
|
-
|
|
36709
|
-
throw error48;
|
|
36710
|
-
}
|
|
36886
|
+
rethrowIfFatal(error48);
|
|
36711
36887
|
const errMsg = errorMessage(error48);
|
|
36712
|
-
warn(
|
|
36888
|
+
warn(MODULE7, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
36713
36889
|
return {
|
|
36714
36890
|
repo: `${owner}/${repo}`,
|
|
36715
|
-
lastCommitAt: "",
|
|
36716
|
-
daysSinceLastCommit: 999,
|
|
36717
|
-
openIssuesCount: 0,
|
|
36718
|
-
avgIssueResponseDays: 0,
|
|
36719
|
-
ciStatus: "unknown",
|
|
36720
|
-
isActive: false,
|
|
36721
36891
|
checkFailed: true,
|
|
36722
36892
|
failureReason: errMsg
|
|
36723
36893
|
};
|
|
@@ -36729,18 +36899,27 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36729
36899
|
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
|
|
36730
36900
|
return cached2.guidelines;
|
|
36731
36901
|
}
|
|
36902
|
+
const inflight = guidelinesInflight.get(cacheKey);
|
|
36903
|
+
if (inflight)
|
|
36904
|
+
return inflight;
|
|
36905
|
+
const promise2 = fetchContributionGuidelinesUncached(octokit, owner, repo);
|
|
36906
|
+
guidelinesInflight.set(cacheKey, promise2);
|
|
36907
|
+
try {
|
|
36908
|
+
return await promise2;
|
|
36909
|
+
} finally {
|
|
36910
|
+
guidelinesInflight.delete(cacheKey);
|
|
36911
|
+
}
|
|
36912
|
+
}
|
|
36913
|
+
var guidelinesInflight = /* @__PURE__ */ new Map();
|
|
36914
|
+
async function fetchContributionGuidelinesUncached(octokit, owner, repo) {
|
|
36915
|
+
const cacheKey = `${owner}/${repo}`;
|
|
36732
36916
|
const filesToCheck = [
|
|
36733
36917
|
"CONTRIBUTING.md",
|
|
36734
36918
|
".github/CONTRIBUTING.md",
|
|
36735
36919
|
"docs/CONTRIBUTING.md",
|
|
36736
36920
|
"contributing.md"
|
|
36737
36921
|
];
|
|
36738
|
-
const results = await Promise.allSettled(filesToCheck.map((file2) => octokit
|
|
36739
|
-
if ("content" in data) {
|
|
36740
|
-
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
36741
|
-
}
|
|
36742
|
-
return null;
|
|
36743
|
-
})));
|
|
36922
|
+
const results = await Promise.allSettled(filesToCheck.map((file2) => probeRepoFile(octokit, owner, repo, file2)));
|
|
36744
36923
|
for (const result of results) {
|
|
36745
36924
|
if (result.status !== "rejected")
|
|
36746
36925
|
continue;
|
|
@@ -36748,20 +36927,13 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
|
|
|
36748
36927
|
throw result.reason;
|
|
36749
36928
|
}
|
|
36750
36929
|
}
|
|
36751
|
-
for (
|
|
36752
|
-
|
|
36753
|
-
|
|
36754
|
-
const guidelines = parseContributionGuidelines(result.value);
|
|
36930
|
+
for (const result of results) {
|
|
36931
|
+
if (result.status === "fulfilled" && result.value.text) {
|
|
36932
|
+
const guidelines = parseContributionGuidelines(result.value.text);
|
|
36755
36933
|
guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
|
|
36756
36934
|
pruneCache();
|
|
36757
36935
|
return guidelines;
|
|
36758
36936
|
}
|
|
36759
|
-
if (result.status === "rejected") {
|
|
36760
|
-
const status = getHttpStatusCode(result.reason);
|
|
36761
|
-
if (status !== 404) {
|
|
36762
|
-
warn(MODULE6, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${errorMessage(result.reason)}`);
|
|
36763
|
-
}
|
|
36764
|
-
}
|
|
36765
36937
|
}
|
|
36766
36938
|
guidelinesCache.set(cacheKey, {
|
|
36767
36939
|
guidelines: void 0,
|
|
@@ -36776,7 +36948,7 @@ function parseContributionGuidelines(content) {
|
|
|
36776
36948
|
};
|
|
36777
36949
|
const lowerContent = content.toLowerCase();
|
|
36778
36950
|
if (lowerContent.includes("branch")) {
|
|
36779
|
-
const branchMatch = content.match(/branch[^\n]
|
|
36951
|
+
const branchMatch = content.match(/branch[^\n]{0,200}?(?:named?|format|convention)[^\n]{0,200}?[`"]([^`"\n]{1,100})[`"]/i);
|
|
36780
36952
|
if (branchMatch) {
|
|
36781
36953
|
guidelines.branchNamingConvention = branchMatch[1];
|
|
36782
36954
|
}
|
|
@@ -36784,7 +36956,7 @@ function parseContributionGuidelines(content) {
|
|
|
36784
36956
|
if (lowerContent.includes("conventional commit")) {
|
|
36785
36957
|
guidelines.commitMessageFormat = "conventional commits";
|
|
36786
36958
|
} else if (lowerContent.includes("commit message")) {
|
|
36787
|
-
const commitMatch = content.match(/commit message[^\n]
|
|
36959
|
+
const commitMatch = content.match(/commit message[^\n]{0,200}?[`"]([^`"\n]{1,100})[`"]/i);
|
|
36788
36960
|
if (commitMatch) {
|
|
36789
36961
|
guidelines.commitMessageFormat = commitMatch[1];
|
|
36790
36962
|
}
|
|
@@ -36803,14 +36975,13 @@ function parseContributionGuidelines(content) {
|
|
|
36803
36975
|
guidelines.linter = "RuboCop";
|
|
36804
36976
|
else if (lowerContent.includes("prettier"))
|
|
36805
36977
|
guidelines.formatter = "Prettier";
|
|
36806
|
-
if (lowerContent
|
|
36978
|
+
if (/\bcla\b/.test(lowerContent) || lowerContent.includes("contributor license agreement")) {
|
|
36807
36979
|
guidelines.claRequired = true;
|
|
36808
36980
|
}
|
|
36809
36981
|
return guidelines;
|
|
36810
36982
|
}
|
|
36811
36983
|
|
|
36812
36984
|
// ../core/dist/core/anti-llm-policy.js
|
|
36813
|
-
var MODULE7 = "anti-llm-policy";
|
|
36814
36985
|
var POLICY_SCAN_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
36815
36986
|
var ANTI_LLM_KEYWORDS = [
|
|
36816
36987
|
"no ai-generated",
|
|
@@ -36873,28 +37044,8 @@ var SOURCE_FILE_FAMILIES = [
|
|
|
36873
37044
|
paths: ["README.md", "readme.md", "Readme.md"]
|
|
36874
37045
|
}
|
|
36875
37046
|
];
|
|
36876
|
-
async function fetchFileText(octokit, owner, repo, path5) {
|
|
36877
|
-
try {
|
|
36878
|
-
const { data } = await octokit.repos.getContent({ owner, repo, path: path5 });
|
|
36879
|
-
if ("content" in data && typeof data.content === "string") {
|
|
36880
|
-
return {
|
|
36881
|
-
text: Buffer.from(data.content, "base64").toString("utf-8"),
|
|
36882
|
-
transient: false
|
|
36883
|
-
};
|
|
36884
|
-
}
|
|
36885
|
-
return { text: null, transient: false };
|
|
36886
|
-
} catch (error48) {
|
|
36887
|
-
const status = getHttpStatusCode(error48);
|
|
36888
|
-
if (status === 404)
|
|
36889
|
-
return { text: null, transient: false };
|
|
36890
|
-
if (status === 401 || isRateLimitError(error48))
|
|
36891
|
-
throw error48;
|
|
36892
|
-
warn(MODULE7, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(error48)}`);
|
|
36893
|
-
return { text: null, transient: true };
|
|
36894
|
-
}
|
|
36895
|
-
}
|
|
36896
37047
|
async function fetchFamilyText(octokit, owner, repo, paths) {
|
|
36897
|
-
const results = await Promise.allSettled(paths.map((p) =>
|
|
37048
|
+
const results = await Promise.allSettled(paths.map((p) => probeRepoFile(octokit, owner, repo, p)));
|
|
36898
37049
|
let hadTransientFailure = false;
|
|
36899
37050
|
for (const result of results) {
|
|
36900
37051
|
if (result.status === "fulfilled") {
|
|
@@ -36925,7 +37076,7 @@ function isAntiLLMPolicyResult(value) {
|
|
|
36925
37076
|
}
|
|
36926
37077
|
async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
|
|
36927
37078
|
const cache = getHttpCache();
|
|
36928
|
-
const cacheKey = `anti-llm-policy:${owner}/${repo}
|
|
37079
|
+
const cacheKey = versionedCacheKey(`anti-llm-policy:${owner}/${repo}`);
|
|
36929
37080
|
const cached2 = cache.getIfFresh(cacheKey, POLICY_SCAN_CACHE_TTL_MS);
|
|
36930
37081
|
if (isAntiLLMPolicyResult(cached2))
|
|
36931
37082
|
return cached2;
|
|
@@ -36964,6 +37115,76 @@ async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
|
|
|
36964
37115
|
return noMatch;
|
|
36965
37116
|
}
|
|
36966
37117
|
|
|
37118
|
+
// ../core/dist/core/issue-graphql.js
|
|
37119
|
+
var MODULE8 = "issue-graphql";
|
|
37120
|
+
function issueCoreKey(owner, repo, number4) {
|
|
37121
|
+
return `${owner}/${repo}#${number4}`;
|
|
37122
|
+
}
|
|
37123
|
+
function normalizeNode(node) {
|
|
37124
|
+
return {
|
|
37125
|
+
id: node.databaseId,
|
|
37126
|
+
title: node.title,
|
|
37127
|
+
body: node.body ?? "",
|
|
37128
|
+
state: node.state === "CLOSED" ? "closed" : "open",
|
|
37129
|
+
labels: node.labels.nodes.map((l) => l.name),
|
|
37130
|
+
commentCount: node.comments.totalCount,
|
|
37131
|
+
createdAt: node.createdAt,
|
|
37132
|
+
updatedAt: node.updatedAt
|
|
37133
|
+
};
|
|
37134
|
+
}
|
|
37135
|
+
async function prefetchIssueCores(octokit, issues) {
|
|
37136
|
+
const result = /* @__PURE__ */ new Map();
|
|
37137
|
+
if (issues.length === 0)
|
|
37138
|
+
return result;
|
|
37139
|
+
const unique = [
|
|
37140
|
+
...new Map(issues.map((i) => [issueCoreKey(i.owner, i.repo, i.number), i])).values()
|
|
37141
|
+
];
|
|
37142
|
+
const varDefs = [];
|
|
37143
|
+
const selections = [];
|
|
37144
|
+
const variables = {};
|
|
37145
|
+
unique.forEach((iss, i) => {
|
|
37146
|
+
varDefs.push(`$o${i}: String!, $n${i}: String!, $num${i}: Int!`);
|
|
37147
|
+
variables[`o${i}`] = iss.owner;
|
|
37148
|
+
variables[`n${i}`] = iss.repo;
|
|
37149
|
+
variables[`num${i}`] = iss.number;
|
|
37150
|
+
selections.push(`i${i}: repository(owner: $o${i}, name: $n${i}) {
|
|
37151
|
+
issue(number: $num${i}) {
|
|
37152
|
+
databaseId
|
|
37153
|
+
title
|
|
37154
|
+
body
|
|
37155
|
+
state
|
|
37156
|
+
labels(first: 100) { nodes { name } }
|
|
37157
|
+
comments { totalCount }
|
|
37158
|
+
createdAt
|
|
37159
|
+
updatedAt
|
|
37160
|
+
}
|
|
37161
|
+
}`);
|
|
37162
|
+
});
|
|
37163
|
+
const query = `query batchIssueCores(${varDefs.join(", ")}) {
|
|
37164
|
+
${selections.join("\n")}
|
|
37165
|
+
}`;
|
|
37166
|
+
let data;
|
|
37167
|
+
try {
|
|
37168
|
+
data = await octokit.graphql(query, variables);
|
|
37169
|
+
} catch (err) {
|
|
37170
|
+
rethrowIfFatal(err);
|
|
37171
|
+
const partial2 = err.data;
|
|
37172
|
+
if (partial2) {
|
|
37173
|
+
data = partial2;
|
|
37174
|
+
} else {
|
|
37175
|
+
warn(MODULE8, `GraphQL prefetch failed, falling back to REST: ${errorMessage(err)}`);
|
|
37176
|
+
return result;
|
|
37177
|
+
}
|
|
37178
|
+
}
|
|
37179
|
+
unique.forEach((iss, i) => {
|
|
37180
|
+
const node = data?.[`i${i}`]?.issue;
|
|
37181
|
+
if (!node || node.databaseId == null)
|
|
37182
|
+
return;
|
|
37183
|
+
result.set(issueCoreKey(iss.owner, iss.repo, iss.number), normalizeNode(node));
|
|
37184
|
+
});
|
|
37185
|
+
return result;
|
|
37186
|
+
}
|
|
37187
|
+
|
|
36967
37188
|
// ../core/dist/core/slm-triage.js
|
|
36968
37189
|
var DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
|
|
36969
37190
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -37076,15 +37297,96 @@ function isValidTriageShape(value) {
|
|
|
37076
37297
|
}
|
|
37077
37298
|
|
|
37078
37299
|
// ../core/dist/core/issue-vetting.js
|
|
37079
|
-
var
|
|
37300
|
+
var MODULE9 = "issue-vetting";
|
|
37080
37301
|
var MAX_CONCURRENT_VETTING = 3;
|
|
37081
37302
|
var VETTING_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
37303
|
+
function deriveRecommendation(input) {
|
|
37304
|
+
const notes = [];
|
|
37305
|
+
const reasonsToApprove = [];
|
|
37306
|
+
const reasonsToSkip = [];
|
|
37307
|
+
if (!input.noExistingPR)
|
|
37308
|
+
notes.push(input.ownPR ? "Your PR is already in flight for this issue" : "Existing PR found for this issue");
|
|
37309
|
+
if (!input.notClaimed)
|
|
37310
|
+
notes.push("Issue appears to be claimed by someone");
|
|
37311
|
+
if (input.existingPRInconclusive) {
|
|
37312
|
+
notes.push(`Could not verify absence of existing PRs: ${input.existingPRReason || "API error"}`);
|
|
37313
|
+
}
|
|
37314
|
+
if (input.claimInconclusive) {
|
|
37315
|
+
notes.push(`Could not verify claim status: ${input.claimReason || "API error"}`);
|
|
37316
|
+
}
|
|
37317
|
+
if (input.projectCheckFailed) {
|
|
37318
|
+
notes.push(`Could not verify project activity: ${input.projectFailureReason || "API error"}`);
|
|
37319
|
+
} else if (!input.projectIsActive) {
|
|
37320
|
+
notes.push("Project may be inactive");
|
|
37321
|
+
}
|
|
37322
|
+
if (!input.clearRequirements)
|
|
37323
|
+
notes.push("Issue requirements are unclear");
|
|
37324
|
+
if (!input.contributionGuidelinesFound)
|
|
37325
|
+
notes.push("No CONTRIBUTING.md found");
|
|
37326
|
+
if (!input.noExistingPR)
|
|
37327
|
+
reasonsToSkip.push(input.ownPR ? "You already have a PR in flight" : "Has existing PR");
|
|
37328
|
+
if (!input.notClaimed)
|
|
37329
|
+
reasonsToSkip.push("Already claimed");
|
|
37330
|
+
if (!input.projectIsActive && !input.projectCheckFailed)
|
|
37331
|
+
reasonsToSkip.push("Inactive project");
|
|
37332
|
+
if (!input.clearRequirements)
|
|
37333
|
+
reasonsToSkip.push("Unclear requirements");
|
|
37334
|
+
if (input.noExistingPR)
|
|
37335
|
+
reasonsToApprove.push("No existing PR");
|
|
37336
|
+
if (input.notClaimed)
|
|
37337
|
+
reasonsToApprove.push("Not claimed");
|
|
37338
|
+
if (input.projectIsActive && !input.projectCheckFailed)
|
|
37339
|
+
reasonsToApprove.push("Active project");
|
|
37340
|
+
if (input.clearRequirements)
|
|
37341
|
+
reasonsToApprove.push("Clear requirements");
|
|
37342
|
+
if (input.contributionGuidelinesFound)
|
|
37343
|
+
reasonsToApprove.push("Has contribution guidelines");
|
|
37344
|
+
if (input.effectiveMergedCount > 0) {
|
|
37345
|
+
reasonsToApprove.push(`Trusted project (${input.effectiveMergedCount} PR${input.effectiveMergedCount > 1 ? "s" : ""} merged)`);
|
|
37346
|
+
}
|
|
37347
|
+
if (input.orgHasMergedPRs) {
|
|
37348
|
+
reasonsToApprove.push(`Org affinity (merged PRs in other ${input.orgName} repos)`);
|
|
37349
|
+
}
|
|
37350
|
+
if (input.matchesCategory) {
|
|
37351
|
+
reasonsToApprove.push("Matches preferred project category");
|
|
37352
|
+
}
|
|
37353
|
+
if (input.issueClosed) {
|
|
37354
|
+
reasonsToSkip.push("Issue is closed");
|
|
37355
|
+
}
|
|
37356
|
+
let recommendation;
|
|
37357
|
+
if (input.issueClosed) {
|
|
37358
|
+
recommendation = "skip";
|
|
37359
|
+
} else if (input.ownPR) {
|
|
37360
|
+
recommendation = "skip";
|
|
37361
|
+
} else if (input.passedAllChecks) {
|
|
37362
|
+
recommendation = "approve";
|
|
37363
|
+
} else if (reasonsToSkip.length > 2) {
|
|
37364
|
+
recommendation = "skip";
|
|
37365
|
+
} else {
|
|
37366
|
+
recommendation = "needs_review";
|
|
37367
|
+
}
|
|
37368
|
+
const hasInconclusiveChecks = input.projectCheckFailed || input.existingPRInconclusive || input.claimInconclusive || input.mergedCountInconclusive;
|
|
37369
|
+
if (recommendation === "approve" && hasInconclusiveChecks) {
|
|
37370
|
+
recommendation = "needs_review";
|
|
37371
|
+
notes.push("Recommendation downgraded: one or more checks were inconclusive");
|
|
37372
|
+
}
|
|
37373
|
+
return { notes, reasonsToApprove, reasonsToSkip, recommendation };
|
|
37374
|
+
}
|
|
37082
37375
|
var IssueVetter = class {
|
|
37083
37376
|
octokit;
|
|
37084
37377
|
stateReader;
|
|
37085
|
-
|
|
37378
|
+
budgetTracker;
|
|
37379
|
+
/**
|
|
37380
|
+
* @param octokit - Authenticated Octokit instance
|
|
37381
|
+
* @param stateReader - Read-only scout state interface
|
|
37382
|
+
* @param budgetTracker - Search budget tracker. Defaults to the shared
|
|
37383
|
+
* singleton so existing callers behave identically; inject a per-search
|
|
37384
|
+
* instance to isolate budget accounting in a long-lived concurrent host.
|
|
37385
|
+
*/
|
|
37386
|
+
constructor(octokit, stateReader, budgetTracker = getSearchBudgetTracker()) {
|
|
37086
37387
|
this.octokit = octokit;
|
|
37087
37388
|
this.stateReader = stateReader;
|
|
37389
|
+
this.budgetTracker = budgetTracker;
|
|
37088
37390
|
}
|
|
37089
37391
|
/**
|
|
37090
37392
|
* Vet a specific issue — runs all checks and computes recommendation + viability score.
|
|
@@ -37098,10 +37400,10 @@ var IssueVetter = class {
|
|
|
37098
37400
|
async vetIssue(issueUrl, opts) {
|
|
37099
37401
|
const cache = getHttpCache();
|
|
37100
37402
|
const sigKey = opts?.featureSignals ? `:r${opts.featureSignals.reactions}c${opts.featureSignals.comments}m${opts.featureSignals.hasMilestone ? 1 : 0}o${opts.featureSignals.onRoadmap ? 1 : 0}w${opts.featureSignals.wontfixNoContributor ? 1 : 0}` : "";
|
|
37101
|
-
const cacheKey = `vet:${issueUrl}${sigKey}
|
|
37403
|
+
const cacheKey = versionedCacheKey(`vet:${issueUrl}${sigKey}`);
|
|
37102
37404
|
const cached2 = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
|
|
37103
37405
|
if (cached2 && typeof cached2 === "object" && "issue" in cached2 && "viabilityScore" in cached2) {
|
|
37104
|
-
debug(
|
|
37406
|
+
debug(MODULE9, `Vetting cache hit for ${issueUrl}`);
|
|
37105
37407
|
return cached2;
|
|
37106
37408
|
}
|
|
37107
37409
|
const parsed = parseGitHubUrl(issueUrl);
|
|
@@ -37110,24 +37412,23 @@ var IssueVetter = class {
|
|
|
37110
37412
|
}
|
|
37111
37413
|
const { owner, repo, number: number4 } = parsed;
|
|
37112
37414
|
const repoFullName = `${owner}/${repo}`;
|
|
37113
|
-
const
|
|
37114
|
-
owner,
|
|
37115
|
-
repo,
|
|
37116
|
-
issue_number: number4
|
|
37117
|
-
});
|
|
37415
|
+
const core = opts?.prefetched ?? await this.fetchIssueCore(owner, repo, number4);
|
|
37118
37416
|
const reposWithMergedPRs = this.stateReader.getReposWithMergedPRs();
|
|
37119
37417
|
const hasMergedPRsInRepo = reposWithMergedPRs.includes(repoFullName);
|
|
37120
37418
|
const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
|
|
37121
37419
|
checkNoExistingPR(this.octokit, owner, repo, number4),
|
|
37122
|
-
checkNotClaimed(this.octokit, owner, repo, number4,
|
|
37420
|
+
checkNotClaimed(this.octokit, owner, repo, number4, core.commentCount),
|
|
37123
37421
|
checkProjectHealth(this.octokit, owner, repo),
|
|
37124
37422
|
fetchContributionGuidelines(this.octokit, owner, repo),
|
|
37125
|
-
hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo)
|
|
37423
|
+
hasMergedPRsInRepo ? Promise.resolve(0) : checkUserMergedPRsInRepo(this.octokit, owner, repo, this.budgetTracker)
|
|
37126
37424
|
]);
|
|
37127
37425
|
const antiLLMPolicy = await fetchAndScanAntiLLMPolicy(this.octokit, owner, repo, { contributingText: contributionGuidelines?.rawContent });
|
|
37128
37426
|
const noExistingPR = existingPRCheck.passed;
|
|
37129
37427
|
const notClaimed = claimCheck.passed;
|
|
37130
|
-
const
|
|
37428
|
+
const username = this.stateReader.getGitHubUsername?.() ?? "";
|
|
37429
|
+
const linkedPR = existingPRCheck.linkedPR;
|
|
37430
|
+
const ownPR = !noExistingPR && !!linkedPR && linkedPR.state === "open" && !linkedPR.merged && username !== "" && linkedPR.author.toLowerCase() === username.toLowerCase();
|
|
37431
|
+
const clearRequirements = analyzeRequirements(core.body);
|
|
37131
37432
|
const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
|
|
37132
37433
|
const vettingResult = {
|
|
37133
37434
|
passedAllChecks: noExistingPR && notClaimed && projectActive && clearRequirements,
|
|
@@ -37142,89 +37443,52 @@ var IssueVetter = class {
|
|
|
37142
37443
|
linkedPR: existingPRCheck.linkedPR,
|
|
37143
37444
|
notes: []
|
|
37144
37445
|
};
|
|
37145
|
-
if (!noExistingPR)
|
|
37146
|
-
vettingResult.notes.push("Existing PR found for this issue");
|
|
37147
|
-
if (!notClaimed)
|
|
37148
|
-
vettingResult.notes.push("Issue appears to be claimed by someone");
|
|
37149
|
-
if (existingPRCheck.inconclusive) {
|
|
37150
|
-
vettingResult.notes.push(`Could not verify absence of existing PRs: ${existingPRCheck.reason || "API error"}`);
|
|
37151
|
-
}
|
|
37152
|
-
if (claimCheck.inconclusive) {
|
|
37153
|
-
vettingResult.notes.push(`Could not verify claim status: ${claimCheck.reason || "API error"}`);
|
|
37154
|
-
}
|
|
37155
|
-
if (projectHealth.checkFailed) {
|
|
37156
|
-
vettingResult.notes.push(`Could not verify project activity: ${projectHealth.failureReason || "API error"}`);
|
|
37157
|
-
} else if (!projectHealth.isActive) {
|
|
37158
|
-
vettingResult.notes.push("Project may be inactive");
|
|
37159
|
-
}
|
|
37160
|
-
if (!clearRequirements)
|
|
37161
|
-
vettingResult.notes.push("Issue requirements are unclear");
|
|
37162
|
-
if (!contributionGuidelines)
|
|
37163
|
-
vettingResult.notes.push("No CONTRIBUTING.md found");
|
|
37164
37446
|
const trackedIssue = {
|
|
37165
|
-
id:
|
|
37447
|
+
id: core.id,
|
|
37166
37448
|
url: issueUrl,
|
|
37167
37449
|
repo: repoFullName,
|
|
37168
37450
|
number: number4,
|
|
37169
|
-
title:
|
|
37451
|
+
title: core.title,
|
|
37170
37452
|
status: "candidate",
|
|
37171
|
-
labels:
|
|
37172
|
-
createdAt:
|
|
37173
|
-
updatedAt:
|
|
37453
|
+
labels: core.labels,
|
|
37454
|
+
createdAt: core.createdAt,
|
|
37455
|
+
updatedAt: core.updatedAt,
|
|
37174
37456
|
vetted: true,
|
|
37175
37457
|
vettingResult
|
|
37176
37458
|
};
|
|
37177
|
-
const
|
|
37178
|
-
const
|
|
37179
|
-
if (!noExistingPR)
|
|
37180
|
-
reasonsToSkip.push("Has existing PR");
|
|
37181
|
-
if (!notClaimed)
|
|
37182
|
-
reasonsToSkip.push("Already claimed");
|
|
37183
|
-
if (!projectHealth.isActive && !projectHealth.checkFailed)
|
|
37184
|
-
reasonsToSkip.push("Inactive project");
|
|
37185
|
-
if (!clearRequirements)
|
|
37186
|
-
reasonsToSkip.push("Unclear requirements");
|
|
37187
|
-
if (noExistingPR)
|
|
37188
|
-
reasonsToApprove.push("No existing PR");
|
|
37189
|
-
if (notClaimed)
|
|
37190
|
-
reasonsToApprove.push("Not claimed");
|
|
37191
|
-
if (projectHealth.isActive && !projectHealth.checkFailed)
|
|
37192
|
-
reasonsToApprove.push("Active project");
|
|
37193
|
-
if (clearRequirements)
|
|
37194
|
-
reasonsToApprove.push("Clear requirements");
|
|
37195
|
-
if (contributionGuidelines)
|
|
37196
|
-
reasonsToApprove.push("Has contribution guidelines");
|
|
37197
|
-
const effectiveMergedCount = hasMergedPRsInRepo ? 1 : userMergedPRCount;
|
|
37198
|
-
if (effectiveMergedCount > 0) {
|
|
37199
|
-
reasonsToApprove.push(`Trusted project (${effectiveMergedCount} PR${effectiveMergedCount > 1 ? "s" : ""} merged)`);
|
|
37200
|
-
}
|
|
37459
|
+
const mergedCountInconclusive = !hasMergedPRsInRepo && userMergedPRCount === null;
|
|
37460
|
+
const effectiveMergedCount = hasMergedPRsInRepo ? 1 : userMergedPRCount ?? 0;
|
|
37201
37461
|
const orgName = repoFullName.split("/")[0];
|
|
37202
37462
|
let orgHasMergedPRs = false;
|
|
37203
37463
|
if (orgName && repoFullName.includes("/")) {
|
|
37204
37464
|
orgHasMergedPRs = reposWithMergedPRs.some((r) => r.startsWith(orgName + "/") && r !== repoFullName);
|
|
37205
37465
|
}
|
|
37206
|
-
|
|
37207
|
-
|
|
37208
|
-
|
|
37209
|
-
const
|
|
37210
|
-
|
|
37211
|
-
|
|
37212
|
-
|
|
37213
|
-
|
|
37214
|
-
|
|
37215
|
-
|
|
37216
|
-
|
|
37217
|
-
|
|
37218
|
-
|
|
37219
|
-
|
|
37220
|
-
|
|
37221
|
-
|
|
37222
|
-
|
|
37223
|
-
|
|
37224
|
-
|
|
37225
|
-
|
|
37226
|
-
|
|
37227
|
-
|
|
37466
|
+
const matchesCategory = repoBelongsToCategory(repoFullName, this.stateReader.getProjectCategories());
|
|
37467
|
+
const issueClosed = core.state === "closed";
|
|
37468
|
+
const hasInconclusiveChecks = projectHealth.checkFailed || !!existingPRCheck.inconclusive || !!claimCheck.inconclusive || mergedCountInconclusive;
|
|
37469
|
+
const { notes, reasonsToApprove, reasonsToSkip, recommendation } = deriveRecommendation({
|
|
37470
|
+
noExistingPR,
|
|
37471
|
+
ownPR,
|
|
37472
|
+
notClaimed,
|
|
37473
|
+
clearRequirements,
|
|
37474
|
+
contributionGuidelinesFound: !!contributionGuidelines,
|
|
37475
|
+
projectIsActive: projectHealth.checkFailed ? false : projectHealth.isActive,
|
|
37476
|
+
projectCheckFailed: !!projectHealth.checkFailed,
|
|
37477
|
+
projectFailureReason: projectHealth.failureReason,
|
|
37478
|
+
existingPRInconclusive: !!existingPRCheck.inconclusive,
|
|
37479
|
+
existingPRReason: existingPRCheck.inconclusive ? existingPRCheck.reason : void 0,
|
|
37480
|
+
claimInconclusive: !!claimCheck.inconclusive,
|
|
37481
|
+
claimReason: claimCheck.inconclusive ? claimCheck.reason : void 0,
|
|
37482
|
+
mergedCountInconclusive,
|
|
37483
|
+
effectiveMergedCount,
|
|
37484
|
+
orgName,
|
|
37485
|
+
orgHasMergedPRs,
|
|
37486
|
+
matchesCategory,
|
|
37487
|
+
issueClosed,
|
|
37488
|
+
passedAllChecks: vettingResult.passedAllChecks
|
|
37489
|
+
});
|
|
37490
|
+
vettingResult.notes = notes;
|
|
37491
|
+
const repoQualityBonus = projectHealth.checkFailed ? 0 : calculateRepoQualityBonus(projectHealth.stargazersCount ?? 0, projectHealth.forksCount ?? 0);
|
|
37228
37492
|
if (projectHealth.checkFailed && repoQualityBonus === 0) {
|
|
37229
37493
|
vettingResult.notes.push("Repo quality bonus unavailable: could not fetch star/fork counts due to API error");
|
|
37230
37494
|
}
|
|
@@ -37235,8 +37499,8 @@ var IssueVetter = class {
|
|
|
37235
37499
|
isClaimed: !notClaimed,
|
|
37236
37500
|
clearRequirements,
|
|
37237
37501
|
hasContributionGuidelines: !!contributionGuidelines,
|
|
37238
|
-
issueUpdatedAt:
|
|
37239
|
-
closedWithoutMergeCount: 0,
|
|
37502
|
+
issueUpdatedAt: core.updatedAt,
|
|
37503
|
+
closedWithoutMergeCount: this.stateReader.getClosedWithoutMergeCount?.(repoFullName) ?? 0,
|
|
37240
37504
|
mergedPRCount: effectiveMergedCount,
|
|
37241
37505
|
orgHasMergedPRs,
|
|
37242
37506
|
repoQualityBonus,
|
|
@@ -37250,22 +37514,20 @@ var IssueVetter = class {
|
|
|
37250
37514
|
} else if (starredRepos.includes(repoFullName)) {
|
|
37251
37515
|
searchPriority = "starred";
|
|
37252
37516
|
}
|
|
37253
|
-
const slmConfig = this.stateReader.getSLMTriageConfig
|
|
37254
|
-
model: "",
|
|
37255
|
-
host: ""
|
|
37256
|
-
};
|
|
37517
|
+
const slmConfig = this.stateReader.getSLMTriageConfig();
|
|
37257
37518
|
let slmTriage = null;
|
|
37258
|
-
if (slmConfig
|
|
37519
|
+
if (slmConfig) {
|
|
37259
37520
|
const slmOpts = { model: slmConfig.model };
|
|
37260
37521
|
if (slmConfig.host)
|
|
37261
37522
|
slmOpts.host = slmConfig.host;
|
|
37262
37523
|
slmTriage = await triageWithSLM(buildTriageInput({
|
|
37263
|
-
issue: { ...trackedIssue, body:
|
|
37524
|
+
issue: { ...trackedIssue, body: core.body },
|
|
37264
37525
|
linkedPR: existingPRCheck.linkedPR ?? null
|
|
37265
37526
|
}), slmOpts);
|
|
37266
37527
|
}
|
|
37267
37528
|
const result = {
|
|
37268
37529
|
issue: trackedIssue,
|
|
37530
|
+
issueState: issueClosed ? "closed" : "open",
|
|
37269
37531
|
vettingResult,
|
|
37270
37532
|
projectHealth,
|
|
37271
37533
|
antiLLMPolicy,
|
|
@@ -37276,9 +37538,33 @@ var IssueVetter = class {
|
|
|
37276
37538
|
viabilityScore,
|
|
37277
37539
|
searchPriority
|
|
37278
37540
|
};
|
|
37279
|
-
|
|
37541
|
+
if (!hasInconclusiveChecks) {
|
|
37542
|
+
cache.set(cacheKey, "", result);
|
|
37543
|
+
}
|
|
37280
37544
|
return result;
|
|
37281
37545
|
}
|
|
37546
|
+
/**
|
|
37547
|
+
* Fetch a single issue's core fields via REST and normalize them to the same
|
|
37548
|
+
* shape as a GraphQL prefetch (#169). The REST fallback path when no
|
|
37549
|
+
* prefetched core was supplied.
|
|
37550
|
+
*/
|
|
37551
|
+
async fetchIssueCore(owner, repo, number4) {
|
|
37552
|
+
const { data: ghIssue } = await this.octokit.issues.get({
|
|
37553
|
+
owner,
|
|
37554
|
+
repo,
|
|
37555
|
+
issue_number: number4
|
|
37556
|
+
});
|
|
37557
|
+
return {
|
|
37558
|
+
id: ghIssue.id,
|
|
37559
|
+
title: ghIssue.title,
|
|
37560
|
+
body: ghIssue.body || "",
|
|
37561
|
+
state: ghIssue.state === "closed" ? "closed" : "open",
|
|
37562
|
+
labels: ghIssue.labels.map((l) => typeof l === "string" ? l : l.name || ""),
|
|
37563
|
+
commentCount: ghIssue.comments,
|
|
37564
|
+
createdAt: ghIssue.created_at,
|
|
37565
|
+
updatedAt: ghIssue.updated_at
|
|
37566
|
+
};
|
|
37567
|
+
}
|
|
37282
37568
|
/**
|
|
37283
37569
|
* Vet multiple issues in parallel with concurrency limit
|
|
37284
37570
|
*/
|
|
@@ -37289,13 +37575,23 @@ var IssueVetter = class {
|
|
|
37289
37575
|
let rateLimitFailures = 0;
|
|
37290
37576
|
let attemptedCount = 0;
|
|
37291
37577
|
let firstAuthError = null;
|
|
37292
|
-
|
|
37578
|
+
const uniqueUrls = [...new Set(urls)];
|
|
37579
|
+
const prefetched = await prefetchIssueCores(this.octokit, uniqueUrls.flatMap((url2) => {
|
|
37580
|
+
const p = parseGitHubUrl(url2);
|
|
37581
|
+
return p && p.type === "issues" ? [{ owner: p.owner, repo: p.repo, number: p.number }] : [];
|
|
37582
|
+
}));
|
|
37583
|
+
const prefetchFor = (url2) => {
|
|
37584
|
+
const p = parseGitHubUrl(url2);
|
|
37585
|
+
return p && p.type === "issues" ? prefetched.get(issueCoreKey(p.owner, p.repo, p.number)) : void 0;
|
|
37586
|
+
};
|
|
37587
|
+
for (const url2 of uniqueUrls) {
|
|
37293
37588
|
if (candidates.length >= maxResults)
|
|
37294
37589
|
break;
|
|
37295
37590
|
if (firstAuthError)
|
|
37296
37591
|
break;
|
|
37297
37592
|
attemptedCount++;
|
|
37298
|
-
const
|
|
37593
|
+
const core = prefetchFor(url2);
|
|
37594
|
+
const task = this.vetIssue(url2, core ? { prefetched: core } : void 0).then((candidate) => {
|
|
37299
37595
|
if (candidates.length < maxResults) {
|
|
37300
37596
|
if (priority) {
|
|
37301
37597
|
candidate.searchPriority = priority;
|
|
@@ -37311,7 +37607,7 @@ var IssueVetter = class {
|
|
|
37311
37607
|
if (isRateLimitError(error48)) {
|
|
37312
37608
|
rateLimitFailures++;
|
|
37313
37609
|
}
|
|
37314
|
-
warn(
|
|
37610
|
+
warn(MODULE9, `Error vetting issue ${url2}:`, errorMessage(error48));
|
|
37315
37611
|
}).finally(() => pending.delete(url2));
|
|
37316
37612
|
pending.set(url2, task);
|
|
37317
37613
|
if (pending.size >= MAX_CONCURRENT_VETTING) {
|
|
@@ -37321,13 +37617,13 @@ var IssueVetter = class {
|
|
|
37321
37617
|
await Promise.allSettled(pending.values());
|
|
37322
37618
|
if (firstAuthError) {
|
|
37323
37619
|
if (candidates.length > 0) {
|
|
37324
|
-
warn(
|
|
37620
|
+
warn(MODULE9, `Auth failed mid-batch after ${candidates.length} successful vet(s) \u2014 discarding partial results`);
|
|
37325
37621
|
}
|
|
37326
37622
|
throw firstAuthError;
|
|
37327
37623
|
}
|
|
37328
37624
|
const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
|
|
37329
37625
|
if (allFailed) {
|
|
37330
|
-
warn(
|
|
37626
|
+
warn(MODULE9, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37331
37627
|
}
|
|
37332
37628
|
return {
|
|
37333
37629
|
candidates: candidates.slice(0, maxResults),
|
|
@@ -37338,14 +37634,14 @@ var IssueVetter = class {
|
|
|
37338
37634
|
};
|
|
37339
37635
|
|
|
37340
37636
|
// ../core/dist/core/search-phases.js
|
|
37341
|
-
var
|
|
37637
|
+
var MODULE10 = "search-phases";
|
|
37342
37638
|
var GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
37343
37639
|
var INTER_QUERY_DELAY_MS = 2e3;
|
|
37344
37640
|
function chunkLabels(labels, reservedOps = 0) {
|
|
37345
37641
|
const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
|
|
37346
37642
|
if (maxPerChunk < 1) {
|
|
37347
37643
|
if (labels.length > 0) {
|
|
37348
|
-
warn(
|
|
37644
|
+
warn(MODULE10, `Label filtering disabled: ${reservedOps} repo/org ORs exceed GitHub's ${GITHUB_MAX_BOOLEAN_OPS} operator limit. All ${labels.length} label(s) dropped from query.`);
|
|
37349
37645
|
}
|
|
37350
37646
|
return [[]];
|
|
37351
37647
|
}
|
|
@@ -37355,7 +37651,7 @@ function chunkLabels(labels, reservedOps = 0) {
|
|
|
37355
37651
|
for (let i = 0; i < labels.length; i += maxPerChunk) {
|
|
37356
37652
|
chunks.push(labels.slice(i, i + maxPerChunk));
|
|
37357
37653
|
}
|
|
37358
|
-
debug(
|
|
37654
|
+
debug(MODULE10, `Split ${labels.length} labels into ${chunks.length} chunks (${reservedOps} ops reserved, max ${maxPerChunk} per chunk)`);
|
|
37359
37655
|
return chunks;
|
|
37360
37656
|
}
|
|
37361
37657
|
function buildLabelQuery(labels) {
|
|
@@ -37387,15 +37683,14 @@ function interleaveArrays(arrays) {
|
|
|
37387
37683
|
return result;
|
|
37388
37684
|
}
|
|
37389
37685
|
var SEARCH_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
37390
|
-
async function cachedSearchIssues(octokit, params) {
|
|
37391
|
-
const cacheKey = `search:${params.q}:${params.sort}:${params.order}:${params.per_page}
|
|
37686
|
+
async function cachedSearchIssues(octokit, params, tracker = getSearchBudgetTracker()) {
|
|
37687
|
+
const cacheKey = versionedCacheKey(`search:${params.q}:${params.sort}:${params.order}:${params.per_page}`);
|
|
37392
37688
|
const cache = getHttpCache();
|
|
37393
37689
|
const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
|
|
37394
37690
|
if (cached2) {
|
|
37395
|
-
debug(
|
|
37691
|
+
debug(MODULE10, `Search cache hit for query`);
|
|
37396
37692
|
return cached2;
|
|
37397
37693
|
}
|
|
37398
|
-
const tracker = getSearchBudgetTracker();
|
|
37399
37694
|
await tracker.waitForBudget();
|
|
37400
37695
|
let data;
|
|
37401
37696
|
try {
|
|
@@ -37407,7 +37702,7 @@ async function cachedSearchIssues(octokit, params) {
|
|
|
37407
37702
|
if (data.items.length > 0) {
|
|
37408
37703
|
cache.set(cacheKey, "", data);
|
|
37409
37704
|
} else {
|
|
37410
|
-
debug(
|
|
37705
|
+
debug(MODULE10, `Skipping cache for empty search result (possible rate limit artifact)`);
|
|
37411
37706
|
}
|
|
37412
37707
|
return data;
|
|
37413
37708
|
}
|
|
@@ -37455,10 +37750,10 @@ async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResul
|
|
|
37455
37750
|
if (getHttpStatusCode(error48) === 401)
|
|
37456
37751
|
throw error48;
|
|
37457
37752
|
if (isRateLimitError(error48)) {
|
|
37458
|
-
warn(
|
|
37753
|
+
warn(MODULE10, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37459
37754
|
break;
|
|
37460
37755
|
}
|
|
37461
|
-
warn(
|
|
37756
|
+
warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37462
37757
|
}
|
|
37463
37758
|
}
|
|
37464
37759
|
return items;
|
|
@@ -37475,16 +37770,27 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
|
|
|
37475
37770
|
const repoFullName = repos[i];
|
|
37476
37771
|
const [owner, repo] = repoFullName.split("/");
|
|
37477
37772
|
try {
|
|
37478
|
-
const
|
|
37479
|
-
|
|
37480
|
-
|
|
37481
|
-
|
|
37482
|
-
|
|
37483
|
-
|
|
37484
|
-
|
|
37485
|
-
|
|
37486
|
-
|
|
37487
|
-
|
|
37773
|
+
const labelFilters = labels.length > 0 ? labels : [void 0];
|
|
37774
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
37775
|
+
const rawIssues = [];
|
|
37776
|
+
for (const label of labelFilters) {
|
|
37777
|
+
const response = await octokit.issues.listForRepo({
|
|
37778
|
+
owner,
|
|
37779
|
+
repo,
|
|
37780
|
+
state: "open",
|
|
37781
|
+
sort: "created",
|
|
37782
|
+
direction: "desc",
|
|
37783
|
+
per_page: 5,
|
|
37784
|
+
...label !== void 0 ? { labels: label } : {}
|
|
37785
|
+
});
|
|
37786
|
+
for (const issue2 of response.data) {
|
|
37787
|
+
if (seenUrls.has(issue2.html_url))
|
|
37788
|
+
continue;
|
|
37789
|
+
seenUrls.add(issue2.html_url);
|
|
37790
|
+
rawIssues.push(issue2);
|
|
37791
|
+
}
|
|
37792
|
+
}
|
|
37793
|
+
const issuesOnly = rawIssues.filter((item) => !("pull_request" in item) && !item.assignee);
|
|
37488
37794
|
const mapped = issuesOnly.map((issue2) => ({
|
|
37489
37795
|
html_url: issue2.html_url,
|
|
37490
37796
|
repository_url: `https://api.github.com/repos/${repoFullName}`,
|
|
@@ -37509,17 +37815,17 @@ async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResu
|
|
|
37509
37815
|
if (isRateLimitError(error48)) {
|
|
37510
37816
|
rateLimitFailures++;
|
|
37511
37817
|
}
|
|
37512
|
-
warn(
|
|
37818
|
+
warn(MODULE10, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37513
37819
|
}
|
|
37514
37820
|
}
|
|
37515
37821
|
const allReposFailed = failedRepos === repos.length && repos.length > 0;
|
|
37516
37822
|
const rateLimitHit = rateLimitFailures > 0;
|
|
37517
37823
|
if (allReposFailed) {
|
|
37518
|
-
warn(
|
|
37824
|
+
warn(MODULE10, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37519
37825
|
}
|
|
37520
37826
|
return { candidates, allReposFailed, rateLimitHit };
|
|
37521
37827
|
}
|
|
37522
|
-
async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage) {
|
|
37828
|
+
async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage, tracker = getSearchBudgetTracker()) {
|
|
37523
37829
|
const labelChunks = chunkLabels(labels, reservedOps);
|
|
37524
37830
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
37525
37831
|
const allItems = [];
|
|
@@ -37532,7 +37838,7 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
|
|
|
37532
37838
|
sort: "created",
|
|
37533
37839
|
order: "desc",
|
|
37534
37840
|
per_page: perPage
|
|
37535
|
-
});
|
|
37841
|
+
}, tracker);
|
|
37536
37842
|
for (const item of data.items) {
|
|
37537
37843
|
if (!seenUrls.has(item.html_url)) {
|
|
37538
37844
|
seenUrls.add(item.html_url);
|
|
@@ -37551,14 +37857,14 @@ function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
|
|
|
37551
37857
|
return [languages.map((l) => `language:${l}`).join(" ")];
|
|
37552
37858
|
return languages.map((l) => `language:${l}`);
|
|
37553
37859
|
}
|
|
37554
|
-
async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
|
|
37860
|
+
async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage, tracker = getSearchBudgetTracker()) {
|
|
37555
37861
|
const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
|
|
37556
37862
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
37557
37863
|
const allItems = [];
|
|
37558
37864
|
for (let i = 0; i < langVariants.length; i++) {
|
|
37559
37865
|
if (i > 0)
|
|
37560
37866
|
await sleep(INTER_QUERY_DELAY_MS);
|
|
37561
|
-
const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage);
|
|
37867
|
+
const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage, tracker);
|
|
37562
37868
|
for (const item of items) {
|
|
37563
37869
|
if (!seenUrls.has(item.html_url)) {
|
|
37564
37870
|
seenUrls.add(item.html_url);
|
|
@@ -37572,7 +37878,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37572
37878
|
const spamRepos = detectLabelFarmingRepos(items);
|
|
37573
37879
|
if (spamRepos.size > 0) {
|
|
37574
37880
|
const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
|
|
37575
|
-
debug(
|
|
37881
|
+
debug(MODULE10, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
|
|
37576
37882
|
}
|
|
37577
37883
|
const itemsToVet = filterIssues(items).filter((item) => {
|
|
37578
37884
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
@@ -37583,7 +37889,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37583
37889
|
return excludedRepoSets.every((s) => !s.has(repoFullName));
|
|
37584
37890
|
}).slice(0, remainingNeeded * 2);
|
|
37585
37891
|
if (itemsToVet.length === 0) {
|
|
37586
|
-
debug(
|
|
37892
|
+
debug(MODULE10, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
|
|
37587
37893
|
return { candidates: [], allVetFailed: false, rateLimitHit: false };
|
|
37588
37894
|
}
|
|
37589
37895
|
const { candidates: results, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(itemsToVet.map((i) => i.html_url), remainingNeeded, "normal");
|
|
@@ -37595,7 +37901,7 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37595
37901
|
});
|
|
37596
37902
|
const starFilteredCount = results.length - starFiltered.length;
|
|
37597
37903
|
if (starFilteredCount > 0) {
|
|
37598
|
-
debug(
|
|
37904
|
+
debug(MODULE10, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
|
|
37599
37905
|
}
|
|
37600
37906
|
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
37601
37907
|
}
|
|
@@ -37603,28 +37909,50 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
37603
37909
|
// ../core/dist/core/personalization.js
|
|
37604
37910
|
var REPO_BOOST = 20;
|
|
37605
37911
|
var LANGUAGE_BOOST = 10;
|
|
37606
|
-
|
|
37607
|
-
|
|
37608
|
-
|
|
37609
|
-
|
|
37610
|
-
|
|
37611
|
-
|
|
37912
|
+
var ISSUE_TYPE_BOOST = 10;
|
|
37913
|
+
var AVOID_PENALTY = 15;
|
|
37914
|
+
function boostScoreOf(candidate) {
|
|
37915
|
+
return candidate.personalization?.kind === "boosted" ? candidate.personalization.score : 0;
|
|
37916
|
+
}
|
|
37917
|
+
function normalizeSet(values) {
|
|
37918
|
+
return new Set((values ?? []).map((v) => v.trim().toLowerCase()).filter(Boolean));
|
|
37919
|
+
}
|
|
37920
|
+
function annotateBoost(candidates, bias = {}) {
|
|
37921
|
+
const langSet = normalizeSet(bias.preferLanguages);
|
|
37922
|
+
const repoSet = normalizeSet(bias.preferRepos);
|
|
37923
|
+
const avoidSet = normalizeSet(bias.avoidRepos);
|
|
37924
|
+
const typeSet = normalizeSet(bias.boostIssueTypes);
|
|
37925
|
+
if (langSet.size === 0 && repoSet.size === 0 && avoidSet.size === 0 && typeSet.size === 0) {
|
|
37926
|
+
return candidates;
|
|
37927
|
+
}
|
|
37928
|
+
return candidates.map((c) => {
|
|
37612
37929
|
let score = 0;
|
|
37613
37930
|
const reasons = [];
|
|
37614
|
-
|
|
37931
|
+
const repoLower = c.issue.repo.toLowerCase();
|
|
37932
|
+
if (repoSet.size > 0 && repoSet.has(repoLower)) {
|
|
37615
37933
|
score += REPO_BOOST;
|
|
37616
37934
|
reasons.push(`repo affinity: ${c.issue.repo}`);
|
|
37617
37935
|
}
|
|
37618
|
-
const lang = c.projectHealth.language;
|
|
37936
|
+
const lang = c.projectHealth.checkFailed ? null : c.projectHealth.language;
|
|
37619
37937
|
if (langSet.size > 0 && lang && langSet.has(lang.toLowerCase())) {
|
|
37620
37938
|
score += LANGUAGE_BOOST;
|
|
37621
37939
|
reasons.push(`language match: ${lang}`);
|
|
37622
37940
|
}
|
|
37623
|
-
if (
|
|
37624
|
-
c.
|
|
37625
|
-
|
|
37941
|
+
if (typeSet.size > 0) {
|
|
37942
|
+
const matched = c.issue.labels.find((l) => typeSet.has(l.toLowerCase()));
|
|
37943
|
+
if (matched) {
|
|
37944
|
+
score += ISSUE_TYPE_BOOST;
|
|
37945
|
+
reasons.push(`issue type: ${matched}`);
|
|
37946
|
+
}
|
|
37947
|
+
}
|
|
37948
|
+
if (avoidSet.size > 0 && avoidSet.has(repoLower)) {
|
|
37949
|
+
score -= AVOID_PENALTY;
|
|
37950
|
+
reasons.push(`avoided repo: ${c.issue.repo}`);
|
|
37626
37951
|
}
|
|
37627
|
-
|
|
37952
|
+
if (reasons.length === 0)
|
|
37953
|
+
return c;
|
|
37954
|
+
return { ...c, personalization: { kind: "boosted", score, reasons } };
|
|
37955
|
+
});
|
|
37628
37956
|
}
|
|
37629
37957
|
function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
37630
37958
|
if (maxResults <= 0)
|
|
@@ -37649,10 +37977,9 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
|
37649
37977
|
break;
|
|
37650
37978
|
if (seen.has(c.issue.url))
|
|
37651
37979
|
continue;
|
|
37652
|
-
if (c
|
|
37980
|
+
if (boostScoreOf(c) !== 0)
|
|
37653
37981
|
continue;
|
|
37654
|
-
|
|
37655
|
-
picks.push(c);
|
|
37982
|
+
picks.push({ ...c, personalization: { kind: "diversity" } });
|
|
37656
37983
|
seen.add(c.issue.url);
|
|
37657
37984
|
}
|
|
37658
37985
|
for (const c of candidates) {
|
|
@@ -37667,7 +37994,7 @@ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
|
|
|
37667
37994
|
}
|
|
37668
37995
|
|
|
37669
37996
|
// ../core/dist/core/issue-discovery.js
|
|
37670
|
-
var
|
|
37997
|
+
var MODULE11 = "issue-discovery";
|
|
37671
37998
|
var LOW_BUDGET_THRESHOLD = 20;
|
|
37672
37999
|
var CRITICAL_BUDGET_THRESHOLD = 10;
|
|
37673
38000
|
function buildIssueFilter(config2) {
|
|
@@ -37676,16 +38003,17 @@ function buildIssueFilter(config2) {
|
|
|
37676
38003
|
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
37677
38004
|
if (!repoFullName)
|
|
37678
38005
|
return false;
|
|
37679
|
-
|
|
38006
|
+
const repoLower = repoFullName.toLowerCase();
|
|
38007
|
+
if (config2.excludedRepos.has(repoLower))
|
|
37680
38008
|
return false;
|
|
37681
38009
|
if (config2.excludeOrgs.size > 0) {
|
|
37682
|
-
const orgName =
|
|
38010
|
+
const orgName = repoLower.split("/")[0];
|
|
37683
38011
|
if (orgName && config2.excludeOrgs.has(orgName))
|
|
37684
38012
|
return false;
|
|
37685
38013
|
}
|
|
37686
|
-
if (config2.aiBlocklisted.has(
|
|
38014
|
+
if (config2.aiBlocklisted.has(repoLower))
|
|
37687
38015
|
return false;
|
|
37688
|
-
if (config2.lowScoringRepos.has(
|
|
38016
|
+
if (config2.lowScoringRepos.has(repoLower))
|
|
37689
38017
|
return false;
|
|
37690
38018
|
if (config2.skippedUrls.has(item.html_url))
|
|
37691
38019
|
return false;
|
|
@@ -37700,9 +38028,9 @@ function buildIssueFilter(config2) {
|
|
|
37700
38028
|
};
|
|
37701
38029
|
}
|
|
37702
38030
|
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
37703
|
-
info(
|
|
38031
|
+
info(MODULE11, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
37704
38032
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
37705
|
-
info(
|
|
38033
|
+
info(MODULE11, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
37706
38034
|
return {
|
|
37707
38035
|
candidates,
|
|
37708
38036
|
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
@@ -37710,26 +38038,26 @@ async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
|
37710
38038
|
};
|
|
37711
38039
|
}
|
|
37712
38040
|
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
37713
|
-
info(
|
|
38041
|
+
info(MODULE11, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37714
38042
|
const phase1Labels = labels.slice(0, 3);
|
|
37715
38043
|
const reposToSearch = repos.slice(0, 10);
|
|
37716
38044
|
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
37717
|
-
info(
|
|
38045
|
+
info(MODULE11, `Found ${candidates.length} candidates from starred repos`);
|
|
37718
38046
|
return {
|
|
37719
38047
|
candidates,
|
|
37720
38048
|
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
37721
38049
|
rateLimitHit
|
|
37722
38050
|
};
|
|
37723
38051
|
}
|
|
37724
|
-
async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
37725
|
-
info(
|
|
38052
|
+
async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues, tracker) {
|
|
38053
|
+
info(MODULE11, "Phase 2: General issue search...");
|
|
37726
38054
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37727
38055
|
const tierLabelGroups = [];
|
|
37728
38056
|
if (scopes && scopes.length > 1) {
|
|
37729
38057
|
for (const scope of scopes) {
|
|
37730
38058
|
const scopeLabels = SCOPE_LABELS[scope] ?? [];
|
|
37731
38059
|
if (scopeLabels.length === 0) {
|
|
37732
|
-
warn(
|
|
38060
|
+
warn(MODULE11, `Scope "${scope}" has no labels, skipping tier`);
|
|
37733
38061
|
continue;
|
|
37734
38062
|
}
|
|
37735
38063
|
tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
|
|
@@ -37748,8 +38076,8 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37748
38076
|
let rateLimitHit = false;
|
|
37749
38077
|
for (const { tier, tierLabels } of tierLabelGroups) {
|
|
37750
38078
|
try {
|
|
37751
|
-
const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
|
|
37752
|
-
info(
|
|
38079
|
+
const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3, tracker);
|
|
38080
|
+
info(MODULE11, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
|
|
37753
38081
|
const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
|
|
37754
38082
|
tierResults.push(tierCandidates);
|
|
37755
38083
|
for (const c of tierCandidates)
|
|
@@ -37760,7 +38088,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37760
38088
|
if (vetRateLimitHit) {
|
|
37761
38089
|
rateLimitHit = true;
|
|
37762
38090
|
}
|
|
37763
|
-
info(
|
|
38091
|
+
info(MODULE11, `Found ${tierCandidates.length} candidates from ${tier} tier`);
|
|
37764
38092
|
} catch (err) {
|
|
37765
38093
|
if (getHttpStatusCode(err) === 401)
|
|
37766
38094
|
throw err;
|
|
@@ -37769,13 +38097,13 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37769
38097
|
if (isRateLimitError(err)) {
|
|
37770
38098
|
rateLimitHit = true;
|
|
37771
38099
|
}
|
|
37772
|
-
warn(
|
|
38100
|
+
warn(MODULE11, `Error in ${tier} tier search: ${errMsg}`);
|
|
37773
38101
|
tierResults.push([]);
|
|
37774
38102
|
}
|
|
37775
38103
|
}
|
|
37776
38104
|
const interleaved = interleaveArrays(tierResults);
|
|
37777
38105
|
if (interleaved.length === 0 && error48) {
|
|
37778
|
-
warn(
|
|
38106
|
+
warn(MODULE11, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
|
|
37779
38107
|
}
|
|
37780
38108
|
return {
|
|
37781
38109
|
candidates: interleaved.slice(0, maxResults),
|
|
@@ -37783,18 +38111,18 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, language
|
|
|
37783
38111
|
rateLimitHit
|
|
37784
38112
|
};
|
|
37785
38113
|
}
|
|
37786
|
-
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
37787
|
-
info(
|
|
38114
|
+
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues, tracker) {
|
|
38115
|
+
info(MODULE11, "Phase 3: Searching actively maintained repos...");
|
|
37788
38116
|
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37789
38117
|
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
37790
38118
|
if (eligibleStarred.length > 0) {
|
|
37791
|
-
info(
|
|
38119
|
+
info(MODULE11, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
37792
38120
|
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
37793
38121
|
if (restItems.length > 0) {
|
|
37794
38122
|
try {
|
|
37795
38123
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
37796
38124
|
if (candidates.length > 0) {
|
|
37797
|
-
info(
|
|
38125
|
+
info(MODULE11, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
37798
38126
|
return {
|
|
37799
38127
|
candidates,
|
|
37800
38128
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37804,11 +38132,11 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37804
38132
|
} catch (error48) {
|
|
37805
38133
|
if (getHttpStatusCode(error48) === 401)
|
|
37806
38134
|
throw error48;
|
|
37807
|
-
warn(
|
|
38135
|
+
warn(MODULE11, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
|
|
37808
38136
|
}
|
|
37809
38137
|
}
|
|
37810
38138
|
}
|
|
37811
|
-
info(
|
|
38139
|
+
info(MODULE11, "Phase 3: Falling back to Search API...");
|
|
37812
38140
|
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
37813
38141
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37814
38142
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -37821,10 +38149,10 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37821
38149
|
sort: "updated",
|
|
37822
38150
|
order: "desc",
|
|
37823
38151
|
per_page: maxResults * 3
|
|
37824
|
-
});
|
|
37825
|
-
info(
|
|
38152
|
+
}, tracker);
|
|
38153
|
+
info(MODULE11, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
|
|
37826
38154
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
37827
|
-
info(
|
|
38155
|
+
info(MODULE11, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
37828
38156
|
return {
|
|
37829
38157
|
candidates,
|
|
37830
38158
|
error: allVetFailed ? "all vetting failed" : null,
|
|
@@ -37834,7 +38162,7 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37834
38162
|
if (getHttpStatusCode(error48) === 401)
|
|
37835
38163
|
throw error48;
|
|
37836
38164
|
const errMsg = errorMessage(error48);
|
|
37837
|
-
warn(
|
|
38165
|
+
warn(MODULE11, `Error in maintained-repo search: ${errMsg}`);
|
|
37838
38166
|
return {
|
|
37839
38167
|
candidates: [],
|
|
37840
38168
|
error: errMsg,
|
|
@@ -37848,19 +38176,26 @@ var IssueDiscovery = class {
|
|
|
37848
38176
|
octokit;
|
|
37849
38177
|
githubToken;
|
|
37850
38178
|
vetter;
|
|
38179
|
+
budgetTracker;
|
|
37851
38180
|
/** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
|
|
37852
38181
|
rateLimitWarning = null;
|
|
37853
38182
|
/**
|
|
37854
38183
|
* @param githubToken - GitHub personal access token or token from `gh auth token`
|
|
37855
38184
|
* @param preferences - User's search preferences (languages, labels, scopes, etc.)
|
|
37856
38185
|
* @param stateReader - Read-only interface for accessing scout state (merged PRs, starred repos, etc.)
|
|
38186
|
+
* @param budgetTracker - Search budget tracker. Defaults to the shared
|
|
38187
|
+
* singleton so existing callers behave identically. A long-lived host
|
|
38188
|
+
* serving concurrent searches can inject a per-search instance so one
|
|
38189
|
+
* search's init() no longer resets the budget state of another (the
|
|
38190
|
+
* shared-singleton concurrency hazard, #156).
|
|
37857
38191
|
*/
|
|
37858
|
-
constructor(githubToken, preferences, stateReader) {
|
|
38192
|
+
constructor(githubToken, preferences, stateReader, budgetTracker = getSearchBudgetTracker()) {
|
|
37859
38193
|
this.preferences = preferences;
|
|
37860
38194
|
this.stateReader = stateReader;
|
|
37861
38195
|
this.githubToken = githubToken;
|
|
37862
38196
|
this.octokit = getOctokit(githubToken);
|
|
37863
|
-
this.
|
|
38197
|
+
this.budgetTracker = budgetTracker;
|
|
38198
|
+
this.vetter = new IssueVetter(this.octokit, this.stateReader, this.budgetTracker);
|
|
37864
38199
|
}
|
|
37865
38200
|
/**
|
|
37866
38201
|
* Get starred repos from the state reader.
|
|
@@ -37900,16 +38235,29 @@ var IssueDiscovery = class {
|
|
|
37900
38235
|
const labels = options.labels || (scopes ? buildEffectiveLabels(scopes, config2.labels) : config2.labels);
|
|
37901
38236
|
const maxResults = options.maxResults || 10;
|
|
37902
38237
|
const minStars = config2.minStars ?? 50;
|
|
37903
|
-
const interPhaseDelay = config2.interPhaseDelayMs ?? 3e4;
|
|
38238
|
+
const interPhaseDelay = options.interPhaseDelayMs ?? config2.interPhaseDelayMs ?? 3e4;
|
|
37904
38239
|
const ALL_STRATEGIES = CONCRETE_STRATEGIES;
|
|
37905
|
-
const
|
|
38240
|
+
const pickStrategies = (...candidates) => candidates.find((c) => c && c.length > 0) ?? ["all"];
|
|
38241
|
+
const rawStrategies = pickStrategies(options.strategies, config2.defaultStrategy);
|
|
37906
38242
|
const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
|
|
37907
38243
|
const strategiesUsed = [];
|
|
37908
38244
|
const allCandidates = [];
|
|
37909
38245
|
const phaseErrors = {};
|
|
37910
38246
|
let rateLimitHitDuringSearch = false;
|
|
38247
|
+
const applyInterPhaseDelay = async () => {
|
|
38248
|
+
if (interPhaseDelay > 0) {
|
|
38249
|
+
info(MODULE11, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
38250
|
+
await sleep(interPhaseDelay);
|
|
38251
|
+
}
|
|
38252
|
+
};
|
|
38253
|
+
const recordPhaseResult = (key, result) => {
|
|
38254
|
+
allCandidates.push(...result.candidates);
|
|
38255
|
+
phaseErrors[key] = result.error;
|
|
38256
|
+
if (result.rateLimitHit)
|
|
38257
|
+
rateLimitHitDuringSearch = true;
|
|
38258
|
+
};
|
|
37911
38259
|
this.rateLimitWarning = null;
|
|
37912
|
-
const tracker =
|
|
38260
|
+
const tracker = this.budgetTracker;
|
|
37913
38261
|
let searchBudget = LOW_BUDGET_THRESHOLD - 1;
|
|
37914
38262
|
try {
|
|
37915
38263
|
const rateLimit = await checkRateLimit(this.githubToken);
|
|
@@ -37918,18 +38266,18 @@ var IssueDiscovery = class {
|
|
|
37918
38266
|
if (rateLimit.remaining < 5) {
|
|
37919
38267
|
const resetTime = new Date(rateLimit.resetAt).toLocaleTimeString("en-US", { hour12: false });
|
|
37920
38268
|
this.rateLimitWarning = `GitHub search API quota low (${rateLimit.remaining}/${rateLimit.limit} remaining, resets at ${resetTime}). Search may be slow.`;
|
|
37921
|
-
warn(
|
|
38269
|
+
warn(MODULE11, this.rateLimitWarning);
|
|
37922
38270
|
}
|
|
37923
38271
|
if (searchBudget < CRITICAL_BUDGET_THRESHOLD) {
|
|
37924
|
-
info(
|
|
38272
|
+
info(MODULE11, `Search budget critical (${searchBudget} remaining) \u2014 running only Phase 0`);
|
|
37925
38273
|
} else if (searchBudget < LOW_BUDGET_THRESHOLD) {
|
|
37926
|
-
info(
|
|
38274
|
+
info(MODULE11, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
|
|
37927
38275
|
}
|
|
37928
38276
|
} catch (error48) {
|
|
37929
38277
|
if (getHttpStatusCode(error48) === 401)
|
|
37930
38278
|
throw error48;
|
|
37931
38279
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37932
|
-
warn(
|
|
38280
|
+
warn(MODULE11, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37933
38281
|
}
|
|
37934
38282
|
if (searchBudget <= 0) {
|
|
37935
38283
|
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
@@ -37939,15 +38287,15 @@ var IssueDiscovery = class {
|
|
|
37939
38287
|
const openPRRepos = this.stateReader.getReposWithOpenPRs();
|
|
37940
38288
|
const starredRepos = this.getStarredRepos();
|
|
37941
38289
|
const starredRepoSet = new Set(starredRepos);
|
|
37942
|
-
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
|
|
38290
|
+
const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold).map((r) => r.toLowerCase()));
|
|
37943
38291
|
const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
|
|
37944
38292
|
const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
|
|
37945
|
-
const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
|
|
38293
|
+
const aiBlocklisted = new Set(config2.aiPolicyBlocklist.map((r) => r.toLowerCase()));
|
|
37946
38294
|
if (aiBlocklisted.size > 0) {
|
|
37947
|
-
debug(
|
|
38295
|
+
debug(MODULE11, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
|
|
37948
38296
|
}
|
|
37949
38297
|
const filterIssues = buildIssueFilter({
|
|
37950
|
-
excludedRepos: new Set(config2.excludeRepos),
|
|
38298
|
+
excludedRepos: new Set(config2.excludeRepos.map((r) => r.toLowerCase())),
|
|
37951
38299
|
excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
37952
38300
|
aiBlocklisted,
|
|
37953
38301
|
lowScoringRepos,
|
|
@@ -37971,67 +38319,47 @@ var IssueDiscovery = class {
|
|
|
37971
38319
|
const remaining = maxResults - allCandidates.length;
|
|
37972
38320
|
if (remaining > 0) {
|
|
37973
38321
|
const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
|
|
37974
|
-
|
|
37975
|
-
phaseErrors["0"] = result.error;
|
|
37976
|
-
if (result.rateLimitHit)
|
|
37977
|
-
rateLimitHitDuringSearch = true;
|
|
38322
|
+
recordPhaseResult("0", result);
|
|
37978
38323
|
}
|
|
37979
38324
|
strategiesUsed.push("merged");
|
|
37980
38325
|
}
|
|
37981
38326
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37982
|
-
|
|
37983
|
-
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37984
|
-
await sleep(interPhaseDelay);
|
|
37985
|
-
}
|
|
38327
|
+
await applyInterPhaseDelay();
|
|
37986
38328
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
37987
38329
|
if (reposToSearch.length > 0) {
|
|
37988
38330
|
const remaining = maxResults - allCandidates.length;
|
|
37989
38331
|
if (remaining > 0) {
|
|
37990
38332
|
const result = await runPhase1(this.octokit, this.vetter, reposToSearch, labels, remaining, filterIssues);
|
|
37991
|
-
|
|
37992
|
-
|
|
37993
|
-
if (result.rateLimitHit)
|
|
37994
|
-
rateLimitHitDuringSearch = true;
|
|
38333
|
+
recordPhaseResult("1", result);
|
|
38334
|
+
strategiesUsed.push("starred");
|
|
37995
38335
|
}
|
|
37996
38336
|
}
|
|
37997
|
-
strategiesUsed.push("starred");
|
|
37998
38337
|
}
|
|
37999
|
-
const broadDelay = config2.broadPhaseDelayMs ?? 9e4;
|
|
38000
|
-
const
|
|
38338
|
+
const broadDelay = options.broadPhaseDelayMs ?? config2.broadPhaseDelayMs ?? 9e4;
|
|
38339
|
+
const configuredSkipThreshold = config2.skipBroadWhenSufficientResults ?? 8;
|
|
38340
|
+
const skipThreshold = configuredSkipThreshold > 0 ? Math.min(configuredSkipThreshold, maxResults - 1) : 0;
|
|
38001
38341
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
|
|
38002
38342
|
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
38003
|
-
info(
|
|
38343
|
+
info(MODULE11, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
38004
38344
|
} else {
|
|
38005
|
-
|
|
38006
|
-
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
38007
|
-
await sleep(interPhaseDelay);
|
|
38008
|
-
}
|
|
38345
|
+
await applyInterPhaseDelay();
|
|
38009
38346
|
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
38010
|
-
info(
|
|
38347
|
+
info(MODULE11, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
38011
38348
|
await sleep(broadDelay);
|
|
38012
38349
|
} else if (allCandidates.length === 0) {
|
|
38013
|
-
info(
|
|
38350
|
+
info(MODULE11, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
38014
38351
|
}
|
|
38015
38352
|
const remaining = maxResults - allCandidates.length;
|
|
38016
|
-
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
38017
|
-
|
|
38018
|
-
|
|
38019
|
-
if (result.rateLimitHit)
|
|
38020
|
-
rateLimitHitDuringSearch = true;
|
|
38353
|
+
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues, tracker);
|
|
38354
|
+
recordPhaseResult("2", result);
|
|
38355
|
+
strategiesUsed.push("broad");
|
|
38021
38356
|
}
|
|
38022
|
-
strategiesUsed.push("broad");
|
|
38023
38357
|
}
|
|
38024
38358
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
|
|
38025
|
-
|
|
38026
|
-
info(MODULE10, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
38027
|
-
await sleep(interPhaseDelay);
|
|
38028
|
-
}
|
|
38359
|
+
await applyInterPhaseDelay();
|
|
38029
38360
|
const remaining = maxResults - allCandidates.length;
|
|
38030
|
-
const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues);
|
|
38031
|
-
|
|
38032
|
-
phaseErrors["3"] = result.error;
|
|
38033
|
-
if (result.rateLimitHit)
|
|
38034
|
-
rateLimitHitDuringSearch = true;
|
|
38361
|
+
const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues, tracker);
|
|
38362
|
+
recordPhaseResult("3", result);
|
|
38035
38363
|
strategiesUsed.push("maintained");
|
|
38036
38364
|
}
|
|
38037
38365
|
const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
|
|
@@ -38058,8 +38386,13 @@ var IssueDiscovery = class {
|
|
|
38058
38386
|
if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
|
|
38059
38387
|
this.rateLimitWarning = `Search results may be incomplete: GitHub API rate limits were hit during search.${budgetNote} Found ${allCandidates.length} candidate${allCandidates.length === 1 ? "" : "s"} but some search phases were limited. Try again after the rate limit resets for complete results.`;
|
|
38060
38388
|
}
|
|
38061
|
-
annotateBoost(allCandidates,
|
|
38062
|
-
|
|
38389
|
+
const ranked = annotateBoost(allCandidates, {
|
|
38390
|
+
preferLanguages: options.preferLanguages,
|
|
38391
|
+
preferRepos: options.preferRepos,
|
|
38392
|
+
avoidRepos: options.avoidRepos,
|
|
38393
|
+
boostIssueTypes: options.boostIssueTypes
|
|
38394
|
+
});
|
|
38395
|
+
ranked.sort((a, b) => {
|
|
38063
38396
|
const priorityOrder = {
|
|
38064
38397
|
merged_pr: 0,
|
|
38065
38398
|
starred: 1,
|
|
@@ -38072,14 +38405,14 @@ var IssueDiscovery = class {
|
|
|
38072
38405
|
const recDiff = recommendationOrder[a.recommendation] - recommendationOrder[b.recommendation];
|
|
38073
38406
|
if (recDiff !== 0)
|
|
38074
38407
|
return recDiff;
|
|
38075
|
-
const boostDiff = (b
|
|
38408
|
+
const boostDiff = boostScoreOf(b) - boostScoreOf(a);
|
|
38076
38409
|
if (boostDiff !== 0)
|
|
38077
38410
|
return boostDiff;
|
|
38078
38411
|
return b.viabilityScore - a.viabilityScore;
|
|
38079
38412
|
});
|
|
38080
|
-
const capped = applyPerRepoCap(
|
|
38413
|
+
const capped = applyPerRepoCap(ranked, 2);
|
|
38081
38414
|
const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
|
|
38082
|
-
info(
|
|
38415
|
+
info(MODULE11, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${finalPicks.length} candidates returned`);
|
|
38083
38416
|
return { candidates: finalPicks, strategiesUsed };
|
|
38084
38417
|
}
|
|
38085
38418
|
/**
|
|
@@ -38112,7 +38445,6 @@ var IssueDiscovery = class {
|
|
|
38112
38445
|
};
|
|
38113
38446
|
|
|
38114
38447
|
// ../core/dist/core/roadmap.js
|
|
38115
|
-
var MODULE11 = "roadmap";
|
|
38116
38448
|
var CACHE_TTL_MS2 = 60 * 60 * 1e3;
|
|
38117
38449
|
var ROADMAP_PATHS = [
|
|
38118
38450
|
"ROADMAP.md",
|
|
@@ -38166,24 +38498,27 @@ async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
38166
38498
|
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS2) {
|
|
38167
38499
|
return cached2.refs;
|
|
38168
38500
|
}
|
|
38501
|
+
const inflight = roadmapInflight.get(cacheKey);
|
|
38502
|
+
if (inflight)
|
|
38503
|
+
return inflight;
|
|
38504
|
+
const promise2 = fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey);
|
|
38505
|
+
roadmapInflight.set(cacheKey, promise2);
|
|
38506
|
+
try {
|
|
38507
|
+
return await promise2;
|
|
38508
|
+
} finally {
|
|
38509
|
+
roadmapInflight.delete(cacheKey);
|
|
38510
|
+
}
|
|
38511
|
+
}
|
|
38512
|
+
var roadmapInflight = /* @__PURE__ */ new Map();
|
|
38513
|
+
async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
|
|
38169
38514
|
for (const path5 of ROADMAP_PATHS) {
|
|
38170
|
-
|
|
38171
|
-
|
|
38172
|
-
|
|
38173
|
-
|
|
38174
|
-
|
|
38175
|
-
|
|
38176
|
-
|
|
38177
|
-
pruneCache2();
|
|
38178
|
-
return refs;
|
|
38179
|
-
} catch (err) {
|
|
38180
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38181
|
-
throw err;
|
|
38182
|
-
const status = getHttpStatusCode(err);
|
|
38183
|
-
if (status === 404)
|
|
38184
|
-
continue;
|
|
38185
|
-
warn(MODULE11, `Unexpected error fetching ${path5} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
38186
|
-
}
|
|
38515
|
+
const { text } = await probeRepoFile(octokit, owner, repo, path5);
|
|
38516
|
+
if (!text)
|
|
38517
|
+
continue;
|
|
38518
|
+
const refs = parseRoadmapIssueRefs(text, owner, repo);
|
|
38519
|
+
roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
|
|
38520
|
+
pruneCache2();
|
|
38521
|
+
return refs;
|
|
38187
38522
|
}
|
|
38188
38523
|
const empty = /* @__PURE__ */ new Set();
|
|
38189
38524
|
roadmapCache.set(cacheKey, { refs: empty, fetchedAt: Date.now() });
|
|
@@ -38284,6 +38619,37 @@ function isFeatureIssue(item) {
|
|
|
38284
38619
|
return false;
|
|
38285
38620
|
return labels.some((l) => FEATURE_LABELS.includes(l));
|
|
38286
38621
|
}
|
|
38622
|
+
async function vetAndClassify(item, vetter, roadmapRefs) {
|
|
38623
|
+
const labels = extractLabels(item);
|
|
38624
|
+
const hasMilestone = !!item.milestone;
|
|
38625
|
+
const reactions = item.reactions?.total_count ?? 0;
|
|
38626
|
+
const comments = item.comments ?? 0;
|
|
38627
|
+
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38628
|
+
const useRoadmap = roadmapRefs !== void 0;
|
|
38629
|
+
const onRoadmap = useRoadmap && typeof item.number === "number" && roadmapRefs.has(item.number);
|
|
38630
|
+
let candidate;
|
|
38631
|
+
try {
|
|
38632
|
+
candidate = await vetter.vetIssue(item.html_url, {
|
|
38633
|
+
featureSignals: {
|
|
38634
|
+
reactions,
|
|
38635
|
+
comments,
|
|
38636
|
+
hasMilestone,
|
|
38637
|
+
wontfixNoContributor,
|
|
38638
|
+
...useRoadmap ? { onRoadmap } : {}
|
|
38639
|
+
}
|
|
38640
|
+
});
|
|
38641
|
+
} catch (err) {
|
|
38642
|
+
rethrowIfFatal(err);
|
|
38643
|
+
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38644
|
+
return null;
|
|
38645
|
+
}
|
|
38646
|
+
const horizon = classifyHorizon({
|
|
38647
|
+
hasMilestone,
|
|
38648
|
+
labels,
|
|
38649
|
+
...useRoadmap ? { isOnRoadmap: onRoadmap } : {}
|
|
38650
|
+
});
|
|
38651
|
+
return { ...candidate, horizon };
|
|
38652
|
+
}
|
|
38287
38653
|
async function discoverFeatures(opts) {
|
|
38288
38654
|
const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
|
|
38289
38655
|
if (anchorRepos.length === 0) {
|
|
@@ -38316,42 +38682,15 @@ async function discoverFeatures(opts) {
|
|
|
38316
38682
|
response = listResp;
|
|
38317
38683
|
roadmapRefs = refs;
|
|
38318
38684
|
} catch (err) {
|
|
38319
|
-
|
|
38320
|
-
throw err;
|
|
38685
|
+
rethrowIfFatal(err);
|
|
38321
38686
|
warn(MODULE12, `failed to list issues for ${anchorRepos[i]}: ${errorMessage(err)}`);
|
|
38322
38687
|
continue;
|
|
38323
38688
|
}
|
|
38324
38689
|
const items = response.data.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it));
|
|
38325
38690
|
for (const item of items) {
|
|
38326
|
-
const
|
|
38327
|
-
|
|
38328
|
-
|
|
38329
|
-
const comments = item.comments ?? 0;
|
|
38330
|
-
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38331
|
-
const onRoadmap = typeof item.number === "number" && roadmapRefs.has(item.number);
|
|
38332
|
-
let candidate;
|
|
38333
|
-
try {
|
|
38334
|
-
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38335
|
-
featureSignals: {
|
|
38336
|
-
reactions,
|
|
38337
|
-
comments,
|
|
38338
|
-
hasMilestone,
|
|
38339
|
-
wontfixNoContributor,
|
|
38340
|
-
onRoadmap
|
|
38341
|
-
}
|
|
38342
|
-
});
|
|
38343
|
-
} catch (err) {
|
|
38344
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38345
|
-
throw err;
|
|
38346
|
-
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38347
|
-
continue;
|
|
38348
|
-
}
|
|
38349
|
-
const horizon = classifyHorizon({
|
|
38350
|
-
hasMilestone,
|
|
38351
|
-
labels,
|
|
38352
|
-
isOnRoadmap: onRoadmap
|
|
38353
|
-
});
|
|
38354
|
-
candidates.push({ ...candidate, horizon });
|
|
38691
|
+
const candidate = await vetAndClassify(item, opts.vetter, roadmapRefs);
|
|
38692
|
+
if (candidate)
|
|
38693
|
+
candidates.push(candidate);
|
|
38355
38694
|
}
|
|
38356
38695
|
}
|
|
38357
38696
|
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
@@ -38372,10 +38711,8 @@ function buildBroadFeatureSearchQuery(opts) {
|
|
|
38372
38711
|
for (const excl of FEATURE_EXCLUSION_LABELS) {
|
|
38373
38712
|
parts.push(`-label:"${excl}"`);
|
|
38374
38713
|
}
|
|
38375
|
-
|
|
38376
|
-
|
|
38377
|
-
const langClause = languages.map((l) => `language:${l}`).join(" OR ");
|
|
38378
|
-
parts.push(`(${langClause})`);
|
|
38714
|
+
if (opts.language) {
|
|
38715
|
+
parts.push(`language:${opts.language}`);
|
|
38379
38716
|
}
|
|
38380
38717
|
for (const repo of opts.excludeRepos ?? []) {
|
|
38381
38718
|
parts.push(`-repo:${repo}`);
|
|
@@ -38385,8 +38722,19 @@ function buildBroadFeatureSearchQuery(opts) {
|
|
|
38385
38722
|
}
|
|
38386
38723
|
return parts.join(" ");
|
|
38387
38724
|
}
|
|
38725
|
+
function buildBroadFeatureSearchQueries(opts) {
|
|
38726
|
+
const base = {
|
|
38727
|
+
excludeRepos: opts.excludeRepos,
|
|
38728
|
+
excludeOrgs: opts.excludeOrgs
|
|
38729
|
+
};
|
|
38730
|
+
const languages = (opts.languages ?? []).filter((l) => l && l.toLowerCase() !== "any");
|
|
38731
|
+
if (languages.length === 0) {
|
|
38732
|
+
return [buildBroadFeatureSearchQuery(base)];
|
|
38733
|
+
}
|
|
38734
|
+
return languages.map((language) => buildBroadFeatureSearchQuery({ ...base, language }));
|
|
38735
|
+
}
|
|
38388
38736
|
async function discoverFeaturesBroad(opts) {
|
|
38389
|
-
const
|
|
38737
|
+
const queries = buildBroadFeatureSearchQueries({
|
|
38390
38738
|
languages: opts.languages,
|
|
38391
38739
|
excludeRepos: opts.excludeRepos,
|
|
38392
38740
|
excludeOrgs: opts.excludeOrgs
|
|
@@ -38394,16 +38742,25 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38394
38742
|
const maxToVet = opts.maxToVet ?? DEFAULT_BROAD_MAX_TO_VET;
|
|
38395
38743
|
let items;
|
|
38396
38744
|
try {
|
|
38397
|
-
const
|
|
38398
|
-
|
|
38399
|
-
|
|
38400
|
-
|
|
38401
|
-
|
|
38402
|
-
|
|
38403
|
-
|
|
38745
|
+
const merged = [];
|
|
38746
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
38747
|
+
for (const query of queries) {
|
|
38748
|
+
const data = await cachedSearchIssues(opts.octokit, {
|
|
38749
|
+
q: query,
|
|
38750
|
+
sort: "interactions",
|
|
38751
|
+
order: "desc",
|
|
38752
|
+
per_page: maxToVet
|
|
38753
|
+
});
|
|
38754
|
+
for (const raw of data.items) {
|
|
38755
|
+
if (seenUrls.has(raw.html_url))
|
|
38756
|
+
continue;
|
|
38757
|
+
seenUrls.add(raw.html_url);
|
|
38758
|
+
merged.push(raw);
|
|
38759
|
+
}
|
|
38760
|
+
}
|
|
38761
|
+
items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).slice(0, maxToVet);
|
|
38404
38762
|
} catch (err) {
|
|
38405
|
-
|
|
38406
|
-
throw err;
|
|
38763
|
+
rethrowIfFatal(err);
|
|
38407
38764
|
warn(MODULE12, `broad feature search failed: ${errorMessage(err)}`);
|
|
38408
38765
|
return {
|
|
38409
38766
|
quickWins: [],
|
|
@@ -38414,32 +38771,9 @@ async function discoverFeaturesBroad(opts) {
|
|
|
38414
38771
|
}
|
|
38415
38772
|
const candidates = [];
|
|
38416
38773
|
for (const item of items) {
|
|
38417
|
-
const
|
|
38418
|
-
|
|
38419
|
-
|
|
38420
|
-
const comments = item.comments ?? 0;
|
|
38421
|
-
const wontfixNoContributor = item.created_at ? detectWontfixNoContributor({ labels, createdAt: item.created_at }) : false;
|
|
38422
|
-
let candidate;
|
|
38423
|
-
try {
|
|
38424
|
-
candidate = await opts.vetter.vetIssue(item.html_url, {
|
|
38425
|
-
featureSignals: {
|
|
38426
|
-
reactions,
|
|
38427
|
-
comments,
|
|
38428
|
-
hasMilestone,
|
|
38429
|
-
wontfixNoContributor
|
|
38430
|
-
// Roadmap scraping is per-repo and would require an extra fetch
|
|
38431
|
-
// per unique repo in the broad result set — deliberately skipped
|
|
38432
|
-
// here to keep the broad path cheap. Anchor mode keeps the bonus.
|
|
38433
|
-
}
|
|
38434
|
-
});
|
|
38435
|
-
} catch (err) {
|
|
38436
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38437
|
-
throw err;
|
|
38438
|
-
warn(MODULE12, `vet failed for ${item.html_url}: ${errorMessage(err)}`);
|
|
38439
|
-
continue;
|
|
38440
|
-
}
|
|
38441
|
-
const horizon = classifyHorizon({ hasMilestone, labels });
|
|
38442
|
-
candidates.push({ ...candidate, horizon });
|
|
38774
|
+
const candidate = await vetAndClassify(item, opts.vetter);
|
|
38775
|
+
if (candidate)
|
|
38776
|
+
candidates.push(candidate);
|
|
38443
38777
|
}
|
|
38444
38778
|
const passing = candidates.filter((c) => c.viabilityScore >= MIN_VIABILITY_SCORE);
|
|
38445
38779
|
const split = splitByHorizon(passing, opts.count, opts.splitRatio);
|
|
@@ -38459,14 +38793,11 @@ var GIST_DESCRIPTION = "oss-scout-state";
|
|
|
38459
38793
|
var GIST_FILENAME = "state.json";
|
|
38460
38794
|
var GIST_ID_FILE = "gist-id";
|
|
38461
38795
|
var CACHE_FILE = "state-cache.json";
|
|
38462
|
-
var SEARCH_MAX_PAGES =
|
|
38796
|
+
var SEARCH_MAX_PAGES = 10;
|
|
38463
38797
|
function classifyDegradedReason(err) {
|
|
38464
38798
|
if (isRateLimitError(err))
|
|
38465
38799
|
return "rate_limit";
|
|
38466
38800
|
const status = getHttpStatusCode(err);
|
|
38467
|
-
if (status === 403 && errorMessage(err).toLowerCase().includes("abuse detection")) {
|
|
38468
|
-
return "rate_limit";
|
|
38469
|
-
}
|
|
38470
38801
|
if (status !== void 0 && status >= 500 && status < 600)
|
|
38471
38802
|
return "server";
|
|
38472
38803
|
if (err && typeof err === "object" && "code" in err) {
|
|
@@ -38510,12 +38841,23 @@ var GistStateStore = class {
|
|
|
38510
38841
|
* Push state to the gist. Also writes to local cache as fallback.
|
|
38511
38842
|
*/
|
|
38512
38843
|
async push(state) {
|
|
38513
|
-
this.writeCache(state);
|
|
38514
38844
|
if (!this.gistId) {
|
|
38515
38845
|
warn(MODULE13, "No gist ID \u2014 cannot push");
|
|
38846
|
+
this.writeCache(state);
|
|
38516
38847
|
return false;
|
|
38517
38848
|
}
|
|
38518
|
-
|
|
38849
|
+
let toWrite = state;
|
|
38850
|
+
try {
|
|
38851
|
+
const remote = await this.fetchGistState(this.gistId);
|
|
38852
|
+
if (remote) {
|
|
38853
|
+
toWrite = mergeStates(state, remote);
|
|
38854
|
+
}
|
|
38855
|
+
} catch (err) {
|
|
38856
|
+
rethrowIfFatal(err);
|
|
38857
|
+
warn(MODULE13, `Could not fetch gist before push, writing local snapshot: ${errorMessage(err)}`);
|
|
38858
|
+
}
|
|
38859
|
+
this.writeCache(toWrite);
|
|
38860
|
+
const json2 = JSON.stringify(toWrite, null, 2);
|
|
38519
38861
|
if (json2.length > 9e5) {
|
|
38520
38862
|
warn(MODULE13, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
38521
38863
|
return false;
|
|
@@ -38530,31 +38872,11 @@ var GistStateStore = class {
|
|
|
38530
38872
|
debug(MODULE13, "State pushed to gist");
|
|
38531
38873
|
return true;
|
|
38532
38874
|
} catch (err) {
|
|
38533
|
-
|
|
38534
|
-
throw err;
|
|
38875
|
+
rethrowIfFatal(err);
|
|
38535
38876
|
warn(MODULE13, `Failed to push: ${errorMessage(err)}`);
|
|
38536
38877
|
return false;
|
|
38537
38878
|
}
|
|
38538
38879
|
}
|
|
38539
|
-
/**
|
|
38540
|
-
* Pull state from the gist and merge with local state.
|
|
38541
|
-
*/
|
|
38542
|
-
async pull() {
|
|
38543
|
-
if (!this.gistId)
|
|
38544
|
-
return null;
|
|
38545
|
-
try {
|
|
38546
|
-
const state = await this.fetchGistState(this.gistId);
|
|
38547
|
-
if (state) {
|
|
38548
|
-
this.writeCache(state);
|
|
38549
|
-
}
|
|
38550
|
-
return state;
|
|
38551
|
-
} catch (err) {
|
|
38552
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
38553
|
-
throw err;
|
|
38554
|
-
warn(MODULE13, `Failed to pull: ${errorMessage(err)}`);
|
|
38555
|
-
return null;
|
|
38556
|
-
}
|
|
38557
|
-
}
|
|
38558
38880
|
/** Get the current gist ID (if known). */
|
|
38559
38881
|
getGistId() {
|
|
38560
38882
|
return this.gistId;
|
|
@@ -38565,9 +38887,10 @@ var GistStateStore = class {
|
|
|
38565
38887
|
if (cachedId) {
|
|
38566
38888
|
debug(MODULE13, `Trying cached gist ID: ${cachedId}`);
|
|
38567
38889
|
try {
|
|
38568
|
-
const
|
|
38569
|
-
if (
|
|
38890
|
+
const fetched = await this.fetchGistState(cachedId);
|
|
38891
|
+
if (fetched) {
|
|
38570
38892
|
this.gistId = cachedId;
|
|
38893
|
+
const state = this.mergeCacheInto(fetched);
|
|
38571
38894
|
this.writeCache(state);
|
|
38572
38895
|
return { gistId: cachedId, state, created: false };
|
|
38573
38896
|
}
|
|
@@ -38578,19 +38901,23 @@ var GistStateStore = class {
|
|
|
38578
38901
|
}
|
|
38579
38902
|
debug(MODULE13, "Cached gist ID invalid, searching...");
|
|
38580
38903
|
}
|
|
38581
|
-
const
|
|
38582
|
-
if (
|
|
38583
|
-
debug(MODULE13, `Found gist via search: ${
|
|
38584
|
-
this.saveGistId(
|
|
38585
|
-
this.gistId =
|
|
38586
|
-
const
|
|
38587
|
-
if (
|
|
38904
|
+
const search = await this.searchForGist();
|
|
38905
|
+
if (search.id) {
|
|
38906
|
+
debug(MODULE13, `Found gist via search: ${search.id}`);
|
|
38907
|
+
this.saveGistId(search.id);
|
|
38908
|
+
this.gistId = search.id;
|
|
38909
|
+
const fetched = await this.fetchGistState(search.id);
|
|
38910
|
+
if (fetched) {
|
|
38911
|
+
const state = this.mergeCacheInto(fetched);
|
|
38588
38912
|
this.writeCache(state);
|
|
38589
|
-
return { gistId:
|
|
38913
|
+
return { gistId: search.id, state, created: false };
|
|
38590
38914
|
}
|
|
38591
|
-
warn(MODULE13, `Found existing gist ${
|
|
38915
|
+
warn(MODULE13, `Found existing gist ${search.id} but content failed validation. Using local cache to avoid data loss.`);
|
|
38592
38916
|
return this.bootstrapFromCache("unknown");
|
|
38593
38917
|
}
|
|
38918
|
+
if (!search.exhaustive) {
|
|
38919
|
+
warn(MODULE13, `Scanned the first ${SEARCH_MAX_PAGES * 100} gists without finding an oss-scout state gist, but the account has more. Creating a new state gist; if one already exists, copy its id into the gist-id file in the oss-scout data directory to avoid a duplicate.`);
|
|
38920
|
+
}
|
|
38594
38921
|
debug(MODULE13, "No existing gist found, creating new one");
|
|
38595
38922
|
const freshState = ScoutStateSchema.parse({ version: 1 });
|
|
38596
38923
|
const newId = await this.createGist(freshState);
|
|
@@ -38632,12 +38959,17 @@ var GistStateStore = class {
|
|
|
38632
38959
|
return null;
|
|
38633
38960
|
try {
|
|
38634
38961
|
const parsed = JSON.parse(file2.content);
|
|
38635
|
-
return
|
|
38962
|
+
return parseScoutState(parsed);
|
|
38636
38963
|
} catch (err) {
|
|
38637
38964
|
warn(MODULE13, `Gist content failed validation: ${errorMessage(err)}`);
|
|
38638
38965
|
return null;
|
|
38639
38966
|
}
|
|
38640
38967
|
}
|
|
38968
|
+
/**
|
|
38969
|
+
* Scan the user's gists for the state gist. `exhaustive: false` means the
|
|
38970
|
+
* page cap was hit while pages were still full, so the account may hold
|
|
38971
|
+
* the state gist beyond the scan window.
|
|
38972
|
+
*/
|
|
38641
38973
|
async searchForGist() {
|
|
38642
38974
|
for (let page = 1; page <= SEARCH_MAX_PAGES; page++) {
|
|
38643
38975
|
const { data: gists } = await this.octokit.gists.list({
|
|
@@ -38645,12 +38977,14 @@ var GistStateStore = class {
|
|
|
38645
38977
|
page
|
|
38646
38978
|
});
|
|
38647
38979
|
if (gists.length === 0)
|
|
38648
|
-
|
|
38980
|
+
return { id: null, exhaustive: true };
|
|
38649
38981
|
const match = gists.find((g) => g.description === GIST_DESCRIPTION);
|
|
38650
38982
|
if (match)
|
|
38651
|
-
return match.id;
|
|
38983
|
+
return { id: match.id, exhaustive: true };
|
|
38984
|
+
if (gists.length < 100)
|
|
38985
|
+
return { id: null, exhaustive: true };
|
|
38652
38986
|
}
|
|
38653
|
-
return null;
|
|
38987
|
+
return { id: null, exhaustive: false };
|
|
38654
38988
|
}
|
|
38655
38989
|
async createGist(state) {
|
|
38656
38990
|
const { data } = await this.octokit.gists.create({
|
|
@@ -38678,10 +39012,21 @@ var GistStateStore = class {
|
|
|
38678
39012
|
saveGistId(id) {
|
|
38679
39013
|
fs3.writeFileSync(getGistIdPath(), id + "\n", { mode: 384 });
|
|
38680
39014
|
}
|
|
39015
|
+
/**
|
|
39016
|
+
* Merge the local state-cache into a freshly fetched gist state before it
|
|
39017
|
+
* overwrites the cache (#117). Without this, a prior failed push left its
|
|
39018
|
+
* only copy of the user's changes in state-cache.json, which the next
|
|
39019
|
+
* successful bootstrap silently clobbered. Tombstone-aware mergeStates
|
|
39020
|
+
* keeps both sides' real changes.
|
|
39021
|
+
*/
|
|
39022
|
+
mergeCacheInto(fetched) {
|
|
39023
|
+
const cached2 = this.readCache();
|
|
39024
|
+
return cached2 ? mergeStates(cached2, fetched) : fetched;
|
|
39025
|
+
}
|
|
38681
39026
|
readCache() {
|
|
38682
39027
|
try {
|
|
38683
39028
|
const raw = fs3.readFileSync(getCachePath(), "utf-8");
|
|
38684
|
-
return
|
|
39029
|
+
return parseScoutState(JSON.parse(raw));
|
|
38685
39030
|
} catch (err) {
|
|
38686
39031
|
const code = err?.code;
|
|
38687
39032
|
if (code !== "ENOENT") {
|
|
@@ -38700,18 +39045,57 @@ var GistStateStore = class {
|
|
|
38700
39045
|
}
|
|
38701
39046
|
}
|
|
38702
39047
|
};
|
|
39048
|
+
var TOMBSTONE_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
39049
|
+
function mergeTombstones(local, remote) {
|
|
39050
|
+
const cutoff = Date.now() - TOMBSTONE_TTL_MS;
|
|
39051
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
39052
|
+
for (const t of [...local, ...remote]) {
|
|
39053
|
+
const existing = byUrl.get(t.url);
|
|
39054
|
+
if (!existing || t.removedAt > existing.removedAt)
|
|
39055
|
+
byUrl.set(t.url, t);
|
|
39056
|
+
}
|
|
39057
|
+
return [...byUrl.values()].filter((t) => {
|
|
39058
|
+
const ts = new Date(t.removedAt).getTime();
|
|
39059
|
+
return !Number.isFinite(ts) || ts >= cutoff;
|
|
39060
|
+
});
|
|
39061
|
+
}
|
|
39062
|
+
function applyTombstones(items, tombstones, urlOf, touchedAtOf) {
|
|
39063
|
+
if (tombstones.length === 0)
|
|
39064
|
+
return items;
|
|
39065
|
+
const byUrl = new Map(tombstones.map((t) => [t.url, t.removedAt]));
|
|
39066
|
+
return items.filter((item) => {
|
|
39067
|
+
const removedAt = byUrl.get(urlOf(item));
|
|
39068
|
+
if (removedAt === void 0)
|
|
39069
|
+
return true;
|
|
39070
|
+
return touchedAtOf(item) > removedAt;
|
|
39071
|
+
});
|
|
39072
|
+
}
|
|
38703
39073
|
function mergeStates(local, remote) {
|
|
39074
|
+
const tombstones = mergeTombstones(local.tombstones ?? [], remote.tombstones ?? []);
|
|
39075
|
+
const savedResults = applyTombstones(mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []), tombstones, (r) => r.issueUrl, (r) => r.lastSeenAt);
|
|
39076
|
+
const skippedIssues = applyTombstones(mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []), tombstones, (s) => s.url, (s) => s.skippedAt);
|
|
39077
|
+
const skippedUrls = new Set(skippedIssues.map((s) => s.url));
|
|
39078
|
+
const savedResultsFinal = savedResults.filter((r) => !skippedUrls.has(r.issueUrl));
|
|
39079
|
+
const localPrefsTs = local.preferencesUpdatedAt;
|
|
39080
|
+
const remotePrefsTs = remote.preferencesUpdatedAt;
|
|
39081
|
+
const localPrefsWin = localPrefsTs !== void 0 && (remotePrefsTs === void 0 || localPrefsTs > remotePrefsTs);
|
|
39082
|
+
const preferences = localPrefsWin ? local.preferences : remote.preferences;
|
|
39083
|
+
const preferencesUpdatedAt = pickFresherTimestamp(localPrefsTs, remotePrefsTs);
|
|
38704
39084
|
return {
|
|
39085
|
+
...local,
|
|
39086
|
+
...remote,
|
|
38705
39087
|
version: 1,
|
|
38706
|
-
preferences
|
|
39088
|
+
preferences,
|
|
39089
|
+
preferencesUpdatedAt,
|
|
39090
|
+
tombstones,
|
|
38707
39091
|
repoScores: mergeRepoScores(local.repoScores, remote.repoScores),
|
|
38708
39092
|
starredRepos: mergeStarredRepos(local, remote),
|
|
38709
39093
|
starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
|
|
38710
39094
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
38711
39095
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
38712
39096
|
openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
|
|
38713
|
-
savedResults:
|
|
38714
|
-
skippedIssues
|
|
39097
|
+
savedResults: savedResultsFinal,
|
|
39098
|
+
skippedIssues,
|
|
38715
39099
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
38716
39100
|
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
38717
39101
|
gistId: remote.gistId ?? local.gistId
|
|
@@ -38793,7 +39177,7 @@ function loadLocalState() {
|
|
|
38793
39177
|
const statePath = getStatePath();
|
|
38794
39178
|
try {
|
|
38795
39179
|
const raw = fs4.readFileSync(statePath, "utf-8");
|
|
38796
|
-
return
|
|
39180
|
+
return parseScoutState(JSON.parse(raw));
|
|
38797
39181
|
} catch (err) {
|
|
38798
39182
|
const code = err?.code;
|
|
38799
39183
|
if (code === "ENOENT") {
|
|
@@ -38810,6 +39194,23 @@ function loadLocalState() {
|
|
|
38810
39194
|
return ScoutStateSchema.parse({ version: 1 });
|
|
38811
39195
|
}
|
|
38812
39196
|
}
|
|
39197
|
+
var tmpCounter = 0;
|
|
39198
|
+
function saveLocalState(state) {
|
|
39199
|
+
const statePath = getStatePath();
|
|
39200
|
+
const tmpPath = `${statePath}.tmp.${process.pid}.${tmpCounter++}`;
|
|
39201
|
+
const data = JSON.stringify(state, null, 2) + "\n";
|
|
39202
|
+
fs4.writeFileSync(tmpPath, data, { mode: 384 });
|
|
39203
|
+
try {
|
|
39204
|
+
fs4.renameSync(tmpPath, statePath);
|
|
39205
|
+
} catch (err) {
|
|
39206
|
+
try {
|
|
39207
|
+
fs4.unlinkSync(tmpPath);
|
|
39208
|
+
} catch {
|
|
39209
|
+
}
|
|
39210
|
+
throw err;
|
|
39211
|
+
}
|
|
39212
|
+
debug(MODULE14, "State saved");
|
|
39213
|
+
}
|
|
38813
39214
|
|
|
38814
39215
|
// ../core/dist/scout.js
|
|
38815
39216
|
function offlineModeMessage(reason) {
|
|
@@ -38864,8 +39265,12 @@ function toGistOctokit(octokit) {
|
|
|
38864
39265
|
};
|
|
38865
39266
|
}
|
|
38866
39267
|
async function createScout(config2) {
|
|
39268
|
+
if (config2.logLevel !== void 0) {
|
|
39269
|
+
setLogLevel(config2.logLevel);
|
|
39270
|
+
}
|
|
38867
39271
|
let state;
|
|
38868
39272
|
let gistStore = null;
|
|
39273
|
+
let persistLocal = false;
|
|
38869
39274
|
if (config2.persistence === "provided") {
|
|
38870
39275
|
state = config2.initialState;
|
|
38871
39276
|
} else if (config2.persistence === "gist") {
|
|
@@ -38882,37 +39287,67 @@ async function createScout(config2) {
|
|
|
38882
39287
|
state.gistId = result.gistId;
|
|
38883
39288
|
}
|
|
38884
39289
|
} else {
|
|
38885
|
-
state =
|
|
39290
|
+
state = loadLocalState();
|
|
39291
|
+
persistLocal = true;
|
|
38886
39292
|
}
|
|
38887
|
-
return new OssScout(config2.githubToken, state, gistStore);
|
|
39293
|
+
return new OssScout(config2.githubToken, state, gistStore, { persistLocal });
|
|
38888
39294
|
}
|
|
38889
39295
|
var OssScout = class {
|
|
38890
39296
|
githubToken;
|
|
38891
39297
|
gistStore;
|
|
38892
39298
|
state;
|
|
38893
39299
|
dirty = false;
|
|
38894
|
-
|
|
39300
|
+
/** When true, checkpoint() also writes ~/.oss-scout/state.json. */
|
|
39301
|
+
persistLocal;
|
|
39302
|
+
constructor(githubToken, initialState, gistStore = null, opts = {}) {
|
|
38895
39303
|
this.githubToken = githubToken;
|
|
38896
39304
|
this.gistStore = gistStore;
|
|
38897
39305
|
this.state = initialState;
|
|
39306
|
+
this.persistLocal = opts.persistLocal ?? false;
|
|
38898
39307
|
}
|
|
38899
39308
|
// ── Search ──────────────────────────────────────────────────────────
|
|
39309
|
+
/**
|
|
39310
|
+
* Drop stale disk-cache entries. Called at the top of every cache-burning
|
|
39311
|
+
* entry point (search, features, vetList); without it ~/.oss-scout/cache
|
|
39312
|
+
* grows without bound. evictStale never throws (fs errors degrade to warn).
|
|
39313
|
+
*/
|
|
39314
|
+
evictStaleCacheEntries() {
|
|
39315
|
+
getHttpCache().evictStale();
|
|
39316
|
+
}
|
|
38900
39317
|
/**
|
|
38901
39318
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
38902
39319
|
* Automatically culls expired skip entries and filters skipped issues.
|
|
38903
39320
|
*/
|
|
38904
39321
|
async search(options) {
|
|
39322
|
+
this.evictStaleCacheEntries();
|
|
38905
39323
|
this.cullExpiredSkips();
|
|
38906
39324
|
const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
38907
39325
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
39326
|
+
const prefs = this.state.preferences;
|
|
39327
|
+
const prefLangs = prefs.preferLanguages ?? [];
|
|
39328
|
+
const prefRepos = prefs.preferRepos ?? [];
|
|
39329
|
+
const preferLanguages = options?.preferLanguages ?? (prefLangs.length > 0 ? prefLangs : void 0);
|
|
39330
|
+
const preferRepos = options?.preferRepos ?? (prefRepos.length > 0 ? prefRepos : void 0);
|
|
39331
|
+
const prefAvoid = prefs.avoidRepos ?? [];
|
|
39332
|
+
const prefBoostTypes = prefs.boostIssueTypes ?? [];
|
|
39333
|
+
const avoidRepos = options?.avoidRepos ?? (prefAvoid.length > 0 ? prefAvoid : void 0);
|
|
39334
|
+
const boostIssueTypes = options?.boostIssueTypes ?? (prefBoostTypes.length > 0 ? prefBoostTypes : void 0);
|
|
39335
|
+
const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
|
|
38908
39336
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
38909
39337
|
maxResults: options?.maxResults,
|
|
38910
39338
|
strategies: options?.strategies,
|
|
38911
39339
|
skippedUrls,
|
|
38912
|
-
preferLanguages
|
|
38913
|
-
preferRepos
|
|
38914
|
-
|
|
39340
|
+
preferLanguages,
|
|
39341
|
+
preferRepos,
|
|
39342
|
+
avoidRepos,
|
|
39343
|
+
boostIssueTypes,
|
|
39344
|
+
diversityRatio,
|
|
39345
|
+
interPhaseDelayMs: options?.interPhaseDelayMs,
|
|
39346
|
+
broadPhaseDelayMs: options?.broadPhaseDelayMs
|
|
38915
39347
|
});
|
|
39348
|
+
for (const c of candidates) {
|
|
39349
|
+
this.updateRepoSignalsFromHealth(c.projectHealth);
|
|
39350
|
+
}
|
|
38916
39351
|
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38917
39352
|
this.dirty = true;
|
|
38918
39353
|
return {
|
|
@@ -38923,6 +39358,27 @@ var OssScout = class {
|
|
|
38923
39358
|
strategiesUsed
|
|
38924
39359
|
};
|
|
38925
39360
|
}
|
|
39361
|
+
/**
|
|
39362
|
+
* Populate the `hasActiveMaintainers` repo-score signal from a freshly
|
|
39363
|
+
* computed projectHealth (#167). It was initialized false and never set, so
|
|
39364
|
+
* calculateScore's +1 active-maintainers weight was inert; `isActive`
|
|
39365
|
+
* (recent commit activity) is a real, already-computed proxy.
|
|
39366
|
+
*
|
|
39367
|
+
* `isResponsive` and `avgResponseDays` are deliberately NOT set here, and
|
|
39368
|
+
* `isResponsive` no longer carries a score weight at all (#167): real
|
|
39369
|
+
* responsiveness needs an actual response-time measurement (extra API calls)
|
|
39370
|
+
* that is out of scope, and `hasActiveMaintainers` already covers the
|
|
39371
|
+
* activity proxy. `hasHostileComments` stays a host-settable capability (it
|
|
39372
|
+
* needs comment sentiment, out of scope). A failed health check is skipped so
|
|
39373
|
+
* its neutral-default fields don't pollute the score.
|
|
39374
|
+
*/
|
|
39375
|
+
updateRepoSignalsFromHealth(health) {
|
|
39376
|
+
if (health.checkFailed)
|
|
39377
|
+
return;
|
|
39378
|
+
this.updateRepoScore(health.repo, {
|
|
39379
|
+
signals: { hasActiveMaintainers: health.isActive }
|
|
39380
|
+
});
|
|
39381
|
+
}
|
|
38926
39382
|
/**
|
|
38927
39383
|
* Vet a single issue URL for claimability.
|
|
38928
39384
|
*/
|
|
@@ -38945,6 +39401,7 @@ var OssScout = class {
|
|
|
38945
39401
|
* preferences and excluded repos/orgs.
|
|
38946
39402
|
*/
|
|
38947
39403
|
async features(options) {
|
|
39404
|
+
this.evictStaleCacheEntries();
|
|
38948
39405
|
const count = options?.count ?? 10;
|
|
38949
39406
|
const octokit = getOctokit(this.githubToken);
|
|
38950
39407
|
const vetter = new IssueVetter(octokit, this);
|
|
@@ -38976,6 +39433,7 @@ var OssScout = class {
|
|
|
38976
39433
|
* Optionally prunes unavailable issues from saved results.
|
|
38977
39434
|
*/
|
|
38978
39435
|
async vetList(options) {
|
|
39436
|
+
this.evictStaleCacheEntries();
|
|
38979
39437
|
const saved = this.getSavedResults();
|
|
38980
39438
|
const concurrency = options?.concurrency ?? 5;
|
|
38981
39439
|
const results = [];
|
|
@@ -38991,6 +39449,7 @@ var OssScout = class {
|
|
|
38991
39449
|
number: item.number,
|
|
38992
39450
|
title: item.title,
|
|
38993
39451
|
status: this.classifyVetResult(candidate),
|
|
39452
|
+
ok: true,
|
|
38994
39453
|
recommendation: candidate.recommendation,
|
|
38995
39454
|
viabilityScore: candidate.viabilityScore
|
|
38996
39455
|
});
|
|
@@ -39007,6 +39466,7 @@ var OssScout = class {
|
|
|
39007
39466
|
number: item.number,
|
|
39008
39467
|
title: item.title,
|
|
39009
39468
|
status: isGone ? "closed" : "error",
|
|
39469
|
+
ok: false,
|
|
39010
39470
|
errorMessage: errorMessage(error48)
|
|
39011
39471
|
});
|
|
39012
39472
|
}).finally(() => {
|
|
@@ -39032,17 +39492,41 @@ var OssScout = class {
|
|
|
39032
39492
|
hasPR: results.filter((r) => r.status === "has_pr").length,
|
|
39033
39493
|
errors: results.filter((r) => r.status === "error").length
|
|
39034
39494
|
};
|
|
39495
|
+
const prevStatus = new Map((this.state.savedResults ?? []).map((r) => [r.issueUrl, r.lastStatus]));
|
|
39496
|
+
const transitions = results.filter((r) => r.status !== "error").filter((r) => {
|
|
39497
|
+
const prev = prevStatus.get(r.issueUrl);
|
|
39498
|
+
return prev !== void 0 && prev !== r.status;
|
|
39499
|
+
}).map((r) => ({
|
|
39500
|
+
issueUrl: r.issueUrl,
|
|
39501
|
+
repo: r.repo,
|
|
39502
|
+
number: r.number,
|
|
39503
|
+
from: prevStatus.get(r.issueUrl),
|
|
39504
|
+
to: r.status
|
|
39505
|
+
}));
|
|
39506
|
+
const currentStatus = new Map(results.map((r) => [r.issueUrl, r.status]));
|
|
39507
|
+
for (const saved2 of this.state.savedResults ?? []) {
|
|
39508
|
+
const status = currentStatus.get(saved2.issueUrl);
|
|
39509
|
+
if (status !== void 0 && status !== "error") {
|
|
39510
|
+
saved2.lastStatus = status;
|
|
39511
|
+
}
|
|
39512
|
+
}
|
|
39513
|
+
if (results.length > 0)
|
|
39514
|
+
this.dirty = true;
|
|
39035
39515
|
let prunedCount;
|
|
39036
39516
|
if (options?.prune) {
|
|
39037
39517
|
const unavailableUrls = new Set(results.filter((r) => r.status !== "still_available").map((r) => r.issueUrl));
|
|
39038
39518
|
const before = (this.state.savedResults ?? []).length;
|
|
39039
39519
|
this.state.savedResults = (this.state.savedResults ?? []).filter((r) => !unavailableUrls.has(r.issueUrl));
|
|
39040
39520
|
prunedCount = before - (this.state.savedResults?.length ?? 0);
|
|
39521
|
+
if (prunedCount > 0)
|
|
39522
|
+
this.addTombstones([...unavailableUrls]);
|
|
39041
39523
|
this.dirty = true;
|
|
39042
39524
|
}
|
|
39043
|
-
return { results, summary, prunedCount };
|
|
39525
|
+
return { results, summary, prunedCount, transitions };
|
|
39044
39526
|
}
|
|
39045
39527
|
classifyVetResult(candidate) {
|
|
39528
|
+
if (candidate.issueState === "closed")
|
|
39529
|
+
return "closed";
|
|
39046
39530
|
const checks = candidate.vettingResult.checks;
|
|
39047
39531
|
if (!checks.noExistingPR)
|
|
39048
39532
|
return "has_pr";
|
|
@@ -39077,19 +39561,35 @@ var OssScout = class {
|
|
|
39077
39561
|
getProjectCategories() {
|
|
39078
39562
|
return this.state.preferences.projectCategories;
|
|
39079
39563
|
}
|
|
39564
|
+
/** Configured GitHub username (used to classify own vs competing PRs, #166). */
|
|
39565
|
+
getGitHubUsername() {
|
|
39566
|
+
return this.state.preferences.githubUsername;
|
|
39567
|
+
}
|
|
39080
39568
|
getRepoScore(repo) {
|
|
39081
39569
|
const score = this.state.repoScores[repo];
|
|
39082
39570
|
return score ? score.score : null;
|
|
39083
39571
|
}
|
|
39084
39572
|
/**
|
|
39085
|
-
*
|
|
39086
|
-
*
|
|
39573
|
+
* Number of the user's PRs closed without merge in this repo (#125).
|
|
39574
|
+
* Prefers the tracked repo score; falls back to counting closedPRs so the
|
|
39575
|
+
* scoring penalty works even before a score record exists.
|
|
39576
|
+
*/
|
|
39577
|
+
getClosedWithoutMergeCount(repo) {
|
|
39578
|
+
const score = this.state.repoScores[repo];
|
|
39579
|
+
if (score)
|
|
39580
|
+
return score.closedWithoutMergeCount;
|
|
39581
|
+
return (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
|
|
39582
|
+
}
|
|
39583
|
+
/**
|
|
39584
|
+
* SLM pre-triage config read from preferences (oss-autopilot#1122). Returns
|
|
39585
|
+
* `null` when no `slmTriageModel` is configured — the vetter skips the SLM
|
|
39586
|
+
* call entirely (#158).
|
|
39087
39587
|
*/
|
|
39088
39588
|
getSLMTriageConfig() {
|
|
39089
|
-
|
|
39090
|
-
|
|
39091
|
-
|
|
39092
|
-
};
|
|
39589
|
+
const model = this.state.preferences.slmTriageModel ?? "";
|
|
39590
|
+
if (!model)
|
|
39591
|
+
return null;
|
|
39592
|
+
return { model, host: this.state.preferences.slmTriageHost ?? "" };
|
|
39093
39593
|
}
|
|
39094
39594
|
/** Get current preferences (read-only). */
|
|
39095
39595
|
getPreferences() {
|
|
@@ -39143,6 +39643,73 @@ var OssScout = class {
|
|
|
39143
39643
|
];
|
|
39144
39644
|
this.dirty = true;
|
|
39145
39645
|
}
|
|
39646
|
+
/**
|
|
39647
|
+
* Reconcile tracked open PRs against their current GitHub state (#164).
|
|
39648
|
+
*
|
|
39649
|
+
* `state.openPRs` was append-only — nothing transitioned an open PR to
|
|
39650
|
+
* merged/closed, so getReposWithOpenPRs() over-reported forever once a PR
|
|
39651
|
+
* merged. This checks each open PR, records merges/closures (which updates
|
|
39652
|
+
* the repo score), prunes resolved entries, and checkpoints. Cheaper than a
|
|
39653
|
+
* full bootstrap, so a host can call it on daily startup. Transient errors
|
|
39654
|
+
* leave the entry in place; auth/rate-limit failures propagate.
|
|
39655
|
+
*/
|
|
39656
|
+
async syncOpenPRs() {
|
|
39657
|
+
const octokit = getOctokit(this.githubToken);
|
|
39658
|
+
const open = this.state.openPRs ?? [];
|
|
39659
|
+
const result = {
|
|
39660
|
+
checked: open.length,
|
|
39661
|
+
merged: 0,
|
|
39662
|
+
closed: 0,
|
|
39663
|
+
stillOpen: 0,
|
|
39664
|
+
errors: 0
|
|
39665
|
+
};
|
|
39666
|
+
const remaining = [];
|
|
39667
|
+
for (const pr of open) {
|
|
39668
|
+
const parsed = parseGitHubUrl(pr.url);
|
|
39669
|
+
if (!parsed || parsed.type !== "pull") {
|
|
39670
|
+
remaining.push(pr);
|
|
39671
|
+
result.errors++;
|
|
39672
|
+
continue;
|
|
39673
|
+
}
|
|
39674
|
+
const repoFullName = `${parsed.owner}/${parsed.repo}`;
|
|
39675
|
+
try {
|
|
39676
|
+
const { data } = await octokit.pulls.get({
|
|
39677
|
+
owner: parsed.owner,
|
|
39678
|
+
repo: parsed.repo,
|
|
39679
|
+
pull_number: parsed.number
|
|
39680
|
+
});
|
|
39681
|
+
if (data.merged) {
|
|
39682
|
+
this.recordMergedPR({
|
|
39683
|
+
url: pr.url,
|
|
39684
|
+
title: pr.title,
|
|
39685
|
+
mergedAt: data.merged_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39686
|
+
repo: repoFullName
|
|
39687
|
+
});
|
|
39688
|
+
result.merged++;
|
|
39689
|
+
} else if (data.state === "closed") {
|
|
39690
|
+
this.recordClosedPR({
|
|
39691
|
+
url: pr.url,
|
|
39692
|
+
title: pr.title,
|
|
39693
|
+
closedAt: data.closed_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39694
|
+
repo: repoFullName
|
|
39695
|
+
});
|
|
39696
|
+
result.closed++;
|
|
39697
|
+
} else {
|
|
39698
|
+
remaining.push(pr);
|
|
39699
|
+
result.stillOpen++;
|
|
39700
|
+
}
|
|
39701
|
+
} catch (err) {
|
|
39702
|
+
rethrowIfFatal(err);
|
|
39703
|
+
warn("scout", `sync: could not check ${pr.url}: ${errorMessage(err)}`);
|
|
39704
|
+
remaining.push(pr);
|
|
39705
|
+
result.errors++;
|
|
39706
|
+
}
|
|
39707
|
+
}
|
|
39708
|
+
this.state.openPRs = remaining;
|
|
39709
|
+
this.dirty = true;
|
|
39710
|
+
await this.checkpoint();
|
|
39711
|
+
return result;
|
|
39712
|
+
}
|
|
39146
39713
|
/**
|
|
39147
39714
|
* Update repo score with observed signals.
|
|
39148
39715
|
*/
|
|
@@ -39177,6 +39744,7 @@ var OssScout = class {
|
|
|
39177
39744
|
*/
|
|
39178
39745
|
updatePreferences(updates) {
|
|
39179
39746
|
this.state.preferences = { ...this.state.preferences, ...updates };
|
|
39747
|
+
this.state.preferencesUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
39180
39748
|
this.dirty = true;
|
|
39181
39749
|
}
|
|
39182
39750
|
/**
|
|
@@ -39226,9 +39794,25 @@ var OssScout = class {
|
|
|
39226
39794
|
* Clear all saved results.
|
|
39227
39795
|
*/
|
|
39228
39796
|
clearResults() {
|
|
39797
|
+
this.addTombstones((this.state.savedResults ?? []).map((r) => r.issueUrl));
|
|
39229
39798
|
this.state.savedResults = [];
|
|
39230
39799
|
this.dirty = true;
|
|
39231
39800
|
}
|
|
39801
|
+
/**
|
|
39802
|
+
* Record deletion tombstones (#117) so a later gist merge does not
|
|
39803
|
+
* resurrect these URLs from another machine's copy. A re-add with a newer
|
|
39804
|
+
* timestamp overrides the tombstone in mergeStates.
|
|
39805
|
+
*/
|
|
39806
|
+
addTombstones(urls) {
|
|
39807
|
+
if (urls.length === 0)
|
|
39808
|
+
return;
|
|
39809
|
+
const removedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
39810
|
+
const existing = this.state.tombstones ?? [];
|
|
39811
|
+
const byUrl = new Map(existing.map((t) => [t.url, t]));
|
|
39812
|
+
for (const url2 of urls)
|
|
39813
|
+
byUrl.set(url2, { url: url2, removedAt });
|
|
39814
|
+
this.state.tombstones = [...byUrl.values()];
|
|
39815
|
+
}
|
|
39232
39816
|
// ── Skip List ───────────────────────────────────────────────────────
|
|
39233
39817
|
/**
|
|
39234
39818
|
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
@@ -39262,13 +39846,17 @@ var OssScout = class {
|
|
|
39262
39846
|
* Remove a specific issue from the skip list.
|
|
39263
39847
|
*/
|
|
39264
39848
|
unskipIssue(url2) {
|
|
39849
|
+
const before = (this.state.skippedIssues ?? []).length;
|
|
39265
39850
|
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url2);
|
|
39851
|
+
if (this.state.skippedIssues.length < before)
|
|
39852
|
+
this.addTombstones([url2]);
|
|
39266
39853
|
this.dirty = true;
|
|
39267
39854
|
}
|
|
39268
39855
|
/**
|
|
39269
39856
|
* Clear all skipped issues.
|
|
39270
39857
|
*/
|
|
39271
39858
|
clearSkippedIssues() {
|
|
39859
|
+
this.addTombstones((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
39272
39860
|
this.state.skippedIssues = [];
|
|
39273
39861
|
this.dirty = true;
|
|
39274
39862
|
}
|
|
@@ -39312,6 +39900,13 @@ var OssScout = class {
|
|
|
39312
39900
|
if (!ok)
|
|
39313
39901
|
return false;
|
|
39314
39902
|
}
|
|
39903
|
+
if (this.persistLocal) {
|
|
39904
|
+
try {
|
|
39905
|
+
saveLocalState(this.state);
|
|
39906
|
+
} catch {
|
|
39907
|
+
return false;
|
|
39908
|
+
}
|
|
39909
|
+
}
|
|
39315
39910
|
this.dirty = false;
|
|
39316
39911
|
return true;
|
|
39317
39912
|
}
|
|
@@ -39332,16 +39927,21 @@ var OssScout = class {
|
|
|
39332
39927
|
});
|
|
39333
39928
|
}
|
|
39334
39929
|
/**
|
|
39335
|
-
* Calculate repo score
|
|
39930
|
+
* Calculate repo score from observed data.
|
|
39336
39931
|
* base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
|
|
39337
|
-
* +1
|
|
39932
|
+
* +1 active maintainers, -2 hostile comments, clamped to [1, 10].
|
|
39933
|
+
*
|
|
39934
|
+
* `isResponsive` is intentionally NOT scored (#167): nothing in oss-scout
|
|
39935
|
+
* ever computes it, so awarding +1 for it was dead weight that the
|
|
39936
|
+
* now-computed `hasActiveMaintainers` signal already covers as the activity
|
|
39937
|
+
* proxy. The field is retained on RepoSignals for backward compatibility but
|
|
39938
|
+
* no longer affects the score. With the current signals the reachable range
|
|
39939
|
+
* is [1, 9]; the upper clamp stays as defensive hygiene.
|
|
39338
39940
|
*/
|
|
39339
39941
|
calculateScore(repoScore) {
|
|
39340
39942
|
let score = 5;
|
|
39341
39943
|
score += Math.min(repoScore.mergedPRCount, 3);
|
|
39342
39944
|
score -= Math.min(repoScore.closedWithoutMergeCount, 3);
|
|
39343
|
-
if (repoScore.signals.isResponsive)
|
|
39344
|
-
score += 1;
|
|
39345
39945
|
if (repoScore.signals.hasActiveMaintainers)
|
|
39346
39946
|
score += 1;
|
|
39347
39947
|
if (repoScore.signals.hasHostileComments)
|
|
@@ -39350,8 +39950,158 @@ var OssScout = class {
|
|
|
39350
39950
|
}
|
|
39351
39951
|
};
|
|
39352
39952
|
|
|
39953
|
+
// ../core/dist/core/preference-fields.js
|
|
39954
|
+
var FIELD_CONFIGS = {
|
|
39955
|
+
githubUsername: { type: "string" },
|
|
39956
|
+
languages: { type: "array" },
|
|
39957
|
+
labels: { type: "array" },
|
|
39958
|
+
scope: { type: "enum-array", validValues: IssueScopeSchema.options },
|
|
39959
|
+
excludeRepos: { type: "array" },
|
|
39960
|
+
excludeOrgs: { type: "array" },
|
|
39961
|
+
aiPolicyBlocklist: { type: "array" },
|
|
39962
|
+
projectCategories: {
|
|
39963
|
+
type: "enum-array",
|
|
39964
|
+
validValues: ProjectCategorySchema.options
|
|
39965
|
+
},
|
|
39966
|
+
minStars: { type: "number" },
|
|
39967
|
+
maxIssueAgeDays: { type: "number" },
|
|
39968
|
+
includeDocIssues: { type: "boolean" },
|
|
39969
|
+
minRepoScoreThreshold: { type: "number" },
|
|
39970
|
+
interPhaseDelayMs: { type: "number" },
|
|
39971
|
+
persistence: { type: "enum", validValues: PersistenceModeSchema.options },
|
|
39972
|
+
defaultStrategy: {
|
|
39973
|
+
type: "enum-array",
|
|
39974
|
+
validValues: SearchStrategySchema.options
|
|
39975
|
+
},
|
|
39976
|
+
broadPhaseDelayMs: { type: "number" },
|
|
39977
|
+
skipBroadWhenSufficientResults: { type: "number" },
|
|
39978
|
+
preferLanguages: { type: "array" },
|
|
39979
|
+
preferRepos: { type: "array" },
|
|
39980
|
+
diversityRatio: { type: "float" },
|
|
39981
|
+
avoidRepos: { type: "array" },
|
|
39982
|
+
boostIssueTypes: { type: "array" },
|
|
39983
|
+
slmTriageModel: { type: "string" },
|
|
39984
|
+
slmTriageHost: { type: "string" },
|
|
39985
|
+
featuresAnchorThreshold: { type: "number" },
|
|
39986
|
+
featuresSplitRatio: { type: "float" }
|
|
39987
|
+
};
|
|
39988
|
+
var PREFERENCE_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
39989
|
+
var SORTED_PREFERENCE_KEYS = [
|
|
39990
|
+
...PREFERENCE_KEYS
|
|
39991
|
+
].sort();
|
|
39992
|
+
function parseBoolean(value) {
|
|
39993
|
+
const lower = value.toLowerCase();
|
|
39994
|
+
if (lower === "true" || lower === "yes")
|
|
39995
|
+
return true;
|
|
39996
|
+
if (lower === "false" || lower === "no")
|
|
39997
|
+
return false;
|
|
39998
|
+
throw new ValidationError(`Invalid boolean value: "${value}". Use true/false or yes/no.`);
|
|
39999
|
+
}
|
|
40000
|
+
function parseIntValue(value, key) {
|
|
40001
|
+
const num = parseInt(value, 10);
|
|
40002
|
+
if (isNaN(num)) {
|
|
40003
|
+
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
40004
|
+
}
|
|
40005
|
+
return num;
|
|
40006
|
+
}
|
|
40007
|
+
function parseFloatValue(value, key) {
|
|
40008
|
+
const num = Number.parseFloat(value);
|
|
40009
|
+
if (isNaN(num)) {
|
|
40010
|
+
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
40011
|
+
}
|
|
40012
|
+
return num;
|
|
40013
|
+
}
|
|
40014
|
+
function parseArrayValue(value) {
|
|
40015
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
40016
|
+
}
|
|
40017
|
+
function updateArray(current, value) {
|
|
40018
|
+
if (value.startsWith("+")) {
|
|
40019
|
+
const toAdd = parseArrayValue(value.slice(1));
|
|
40020
|
+
const merged = [...current];
|
|
40021
|
+
for (const item of toAdd) {
|
|
40022
|
+
if (!merged.includes(item))
|
|
40023
|
+
merged.push(item);
|
|
40024
|
+
}
|
|
40025
|
+
return merged;
|
|
40026
|
+
}
|
|
40027
|
+
if (value.startsWith("-")) {
|
|
40028
|
+
const toRemove = new Set(parseArrayValue(value.slice(1)));
|
|
40029
|
+
return current.filter((item) => !toRemove.has(item));
|
|
40030
|
+
}
|
|
40031
|
+
return parseArrayValue(value);
|
|
40032
|
+
}
|
|
40033
|
+
function applyPreferenceField(preferences, key, value) {
|
|
40034
|
+
const field = FIELD_CONFIGS[key];
|
|
40035
|
+
if (!field) {
|
|
40036
|
+
throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${SORTED_PREFERENCE_KEYS.join(", ")}`);
|
|
40037
|
+
}
|
|
40038
|
+
const prefs = { ...preferences };
|
|
40039
|
+
switch (field.type) {
|
|
40040
|
+
case "string":
|
|
40041
|
+
prefs[key] = value;
|
|
40042
|
+
break;
|
|
40043
|
+
case "boolean":
|
|
40044
|
+
prefs[key] = parseBoolean(value);
|
|
40045
|
+
break;
|
|
40046
|
+
case "number":
|
|
40047
|
+
prefs[key] = parseIntValue(value, key);
|
|
40048
|
+
break;
|
|
40049
|
+
case "float":
|
|
40050
|
+
prefs[key] = parseFloatValue(value, key);
|
|
40051
|
+
break;
|
|
40052
|
+
case "array": {
|
|
40053
|
+
const current = prefs[key] ?? [];
|
|
40054
|
+
prefs[key] = updateArray(current, value);
|
|
40055
|
+
break;
|
|
40056
|
+
}
|
|
40057
|
+
case "enum": {
|
|
40058
|
+
const validValues = field.validValues;
|
|
40059
|
+
if (!validValues.includes(value)) {
|
|
40060
|
+
throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(", ")}`);
|
|
40061
|
+
}
|
|
40062
|
+
prefs[key] = value;
|
|
40063
|
+
break;
|
|
40064
|
+
}
|
|
40065
|
+
case "enum-array": {
|
|
40066
|
+
const current = prefs[key] ?? [];
|
|
40067
|
+
const updated = updateArray(current, value);
|
|
40068
|
+
const validValues = field.validValues;
|
|
40069
|
+
const invalid = updated.filter((s) => !validValues.includes(s));
|
|
40070
|
+
if (invalid.length > 0) {
|
|
40071
|
+
throw new ValidationError(`Invalid value(s) for "${key}": ${invalid.join(", ")}. Valid: ${validValues.join(", ")}`);
|
|
40072
|
+
}
|
|
40073
|
+
if (key === "scope") {
|
|
40074
|
+
prefs[key] = updated.length > 0 ? updated : void 0;
|
|
40075
|
+
} else {
|
|
40076
|
+
prefs[key] = updated;
|
|
40077
|
+
}
|
|
40078
|
+
break;
|
|
40079
|
+
}
|
|
40080
|
+
}
|
|
40081
|
+
return ScoutPreferencesSchema.parse(prefs);
|
|
40082
|
+
}
|
|
40083
|
+
|
|
40084
|
+
// ../core/dist/commands/validation.js
|
|
40085
|
+
var ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
|
|
40086
|
+
var MAX_URL_LENGTH = 2048;
|
|
40087
|
+
function validateGitHubUrl(url2, pattern, entityType) {
|
|
40088
|
+
if (pattern.test(url2))
|
|
40089
|
+
return;
|
|
40090
|
+
throw new ValidationError(`Invalid ${entityType} URL: ${url2}. Expected format: https://github.com/owner/repo/issues/123`);
|
|
40091
|
+
}
|
|
40092
|
+
function validateUrl(url2) {
|
|
40093
|
+
if (url2.length > MAX_URL_LENGTH) {
|
|
40094
|
+
throw new ValidationError(`URL exceeds maximum length of ${MAX_URL_LENGTH} characters`);
|
|
40095
|
+
}
|
|
40096
|
+
return url2;
|
|
40097
|
+
}
|
|
40098
|
+
|
|
39353
40099
|
// src/tools.ts
|
|
39354
|
-
var TOOL_TIMEOUT_MS =
|
|
40100
|
+
var TOOL_TIMEOUT_MS = 3e5;
|
|
40101
|
+
var PERSIST_WARNING = "Checkpoint failed: changes were applied in memory but could not be persisted. They may be lost when the server restarts.";
|
|
40102
|
+
function withPersistWarning(payload, persisted) {
|
|
40103
|
+
return persisted ? payload : { ...payload, persistWarning: PERSIST_WARNING };
|
|
40104
|
+
}
|
|
39355
40105
|
function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
|
|
39356
40106
|
let timeoutId;
|
|
39357
40107
|
const timer = new Promise((_, reject) => {
|
|
@@ -39377,6 +40127,12 @@ function registerTools(server, scout) {
|
|
|
39377
40127
|
preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
|
|
39378
40128
|
"Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
|
|
39379
40129
|
),
|
|
40130
|
+
avoidRepos: external_exports3.array(external_exports3.string()).optional().describe(
|
|
40131
|
+
"Soft PENALTY (milder than excludeRepos) for candidates in one of these `owner/repo` slugs (#168). Pushes them down the ranking but does not remove them; a strong boost can still outweigh it."
|
|
40132
|
+
),
|
|
40133
|
+
boostIssueTypes: external_exports3.array(external_exports3.string()).optional().describe(
|
|
40134
|
+
'Soft-boost ranking for candidates whose issue labels match one of these types, case-insensitive (e.g. "bug", "good first issue") (#168). Not a filter; only reorders.'
|
|
40135
|
+
),
|
|
39380
40136
|
diversityRatio: external_exports3.number().min(0).max(1).optional().describe(
|
|
39381
40137
|
"Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."
|
|
39382
40138
|
)
|
|
@@ -39386,6 +40142,8 @@ function registerTools(server, scout) {
|
|
|
39386
40142
|
strategies,
|
|
39387
40143
|
preferLanguages,
|
|
39388
40144
|
preferRepos,
|
|
40145
|
+
avoidRepos,
|
|
40146
|
+
boostIssueTypes,
|
|
39389
40147
|
diversityRatio
|
|
39390
40148
|
}) => {
|
|
39391
40149
|
try {
|
|
@@ -39396,13 +40154,28 @@ function registerTools(server, scout) {
|
|
|
39396
40154
|
strategies: parsedStrategies,
|
|
39397
40155
|
preferLanguages,
|
|
39398
40156
|
preferRepos,
|
|
39399
|
-
|
|
40157
|
+
avoidRepos,
|
|
40158
|
+
boostIssueTypes,
|
|
40159
|
+
diversityRatio,
|
|
40160
|
+
// No fixed inter-phase sleeps in the request/response MCP context;
|
|
40161
|
+
// the budget tracker still paces the GitHub calls (#143)
|
|
40162
|
+
interPhaseDelayMs: 0,
|
|
40163
|
+
broadPhaseDelayMs: 0
|
|
39400
40164
|
})
|
|
39401
40165
|
);
|
|
39402
40166
|
scout.saveResults(result.candidates);
|
|
39403
|
-
await scout.checkpoint();
|
|
40167
|
+
const persisted = await scout.checkpoint();
|
|
39404
40168
|
return {
|
|
39405
|
-
content: [
|
|
40169
|
+
content: [
|
|
40170
|
+
{
|
|
40171
|
+
type: "text",
|
|
40172
|
+
text: JSON.stringify(
|
|
40173
|
+
withPersistWarning(result, persisted),
|
|
40174
|
+
null,
|
|
40175
|
+
2
|
|
40176
|
+
)
|
|
40177
|
+
}
|
|
40178
|
+
]
|
|
39406
40179
|
};
|
|
39407
40180
|
} catch (err) {
|
|
39408
40181
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -39431,9 +40204,18 @@ function registerTools(server, scout) {
|
|
|
39431
40204
|
})
|
|
39432
40205
|
);
|
|
39433
40206
|
scout.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
39434
|
-
await scout.checkpoint();
|
|
40207
|
+
const persisted = await scout.checkpoint();
|
|
39435
40208
|
return {
|
|
39436
|
-
content: [
|
|
40209
|
+
content: [
|
|
40210
|
+
{
|
|
40211
|
+
type: "text",
|
|
40212
|
+
text: JSON.stringify(
|
|
40213
|
+
withPersistWarning(result, persisted),
|
|
40214
|
+
null,
|
|
40215
|
+
2
|
|
40216
|
+
)
|
|
40217
|
+
}
|
|
40218
|
+
]
|
|
39437
40219
|
};
|
|
39438
40220
|
} catch (err) {
|
|
39439
40221
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -39477,6 +40259,24 @@ function registerTools(server, scout) {
|
|
|
39477
40259
|
};
|
|
39478
40260
|
}
|
|
39479
40261
|
);
|
|
40262
|
+
server.tool(
|
|
40263
|
+
"sync",
|
|
40264
|
+
"Reconcile tracked open PRs against GitHub \u2014 mark merged/closed and recompute repo scores. Cheap; good for a daily startup call.",
|
|
40265
|
+
async () => {
|
|
40266
|
+
try {
|
|
40267
|
+
const result = await withTimeout(scout.syncOpenPRs());
|
|
40268
|
+
return {
|
|
40269
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
40270
|
+
};
|
|
40271
|
+
} catch (err) {
|
|
40272
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
40273
|
+
return {
|
|
40274
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
40275
|
+
isError: true
|
|
40276
|
+
};
|
|
40277
|
+
}
|
|
40278
|
+
}
|
|
40279
|
+
);
|
|
39480
40280
|
server.tool(
|
|
39481
40281
|
"skip",
|
|
39482
40282
|
"Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.",
|
|
@@ -39507,7 +40307,7 @@ function registerTools(server, scout) {
|
|
|
39507
40307
|
const count = scout.getSkippedIssues().length;
|
|
39508
40308
|
scout.clearSkippedIssues();
|
|
39509
40309
|
const synced2 = await scout.checkpoint();
|
|
39510
|
-
const syncNote2 = synced2 ? "" : " (
|
|
40310
|
+
const syncNote2 = synced2 ? "" : " (warning: failed to persist to disk)";
|
|
39511
40311
|
return {
|
|
39512
40312
|
content: [
|
|
39513
40313
|
{
|
|
@@ -39521,7 +40321,7 @@ function registerTools(server, scout) {
|
|
|
39521
40321
|
const wasPresent = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
39522
40322
|
scout.unskipIssue(issueUrl);
|
|
39523
40323
|
const synced2 = await scout.checkpoint();
|
|
39524
|
-
const syncNote2 = synced2 ? "" : " (
|
|
40324
|
+
const syncNote2 = synced2 ? "" : " (warning: failed to persist to disk)";
|
|
39525
40325
|
return {
|
|
39526
40326
|
content: [
|
|
39527
40327
|
{
|
|
@@ -39531,6 +40331,8 @@ function registerTools(server, scout) {
|
|
|
39531
40331
|
]
|
|
39532
40332
|
};
|
|
39533
40333
|
}
|
|
40334
|
+
validateUrl(issueUrl);
|
|
40335
|
+
validateGitHubUrl(issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
39534
40336
|
const alreadySkipped = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
39535
40337
|
if (alreadySkipped) {
|
|
39536
40338
|
return {
|
|
@@ -39547,7 +40349,7 @@ function registerTools(server, scout) {
|
|
|
39547
40349
|
} : void 0;
|
|
39548
40350
|
scout.skipIssue(issueUrl, metadata);
|
|
39549
40351
|
const synced = await scout.checkpoint();
|
|
39550
|
-
const syncNote = synced ? "" : " (
|
|
40352
|
+
const syncNote = synced ? "" : " (warning: failed to persist to disk)";
|
|
39551
40353
|
return {
|
|
39552
40354
|
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }]
|
|
39553
40355
|
};
|
|
@@ -39560,105 +40362,44 @@ function registerTools(server, scout) {
|
|
|
39560
40362
|
}
|
|
39561
40363
|
}
|
|
39562
40364
|
);
|
|
39563
|
-
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
39564
|
-
const ARRAY_KEYS = /* @__PURE__ */ new Set([
|
|
39565
|
-
"languages",
|
|
39566
|
-
"labels",
|
|
39567
|
-
"projectCategories",
|
|
39568
|
-
"excludeRepos",
|
|
39569
|
-
"excludeOrgs",
|
|
39570
|
-
"aiPolicyBlocklist",
|
|
39571
|
-
"scope",
|
|
39572
|
-
"defaultStrategy"
|
|
39573
|
-
]);
|
|
39574
|
-
const NUMBER_KEYS = /* @__PURE__ */ new Set([
|
|
39575
|
-
"minStars",
|
|
39576
|
-
"maxIssueAgeDays",
|
|
39577
|
-
"minRepoScoreThreshold",
|
|
39578
|
-
"interPhaseDelayMs",
|
|
39579
|
-
"broadPhaseDelayMs",
|
|
39580
|
-
"skipBroadWhenSufficientResults",
|
|
39581
|
-
"featuresAnchorThreshold",
|
|
39582
|
-
"featuresSplitRatio"
|
|
39583
|
-
]);
|
|
39584
|
-
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
|
|
39585
40365
|
server.tool(
|
|
39586
40366
|
"config-set",
|
|
39587
40367
|
"Update an oss-scout configuration preference",
|
|
39588
40368
|
{
|
|
39589
40369
|
key: external_exports3.string().describe(
|
|
39590
|
-
|
|
40370
|
+
`Preference key to update. One of: ${SORTED_PREFERENCE_KEYS.join(", ")}`
|
|
39591
40371
|
),
|
|
39592
40372
|
value: external_exports3.string().describe(
|
|
39593
|
-
|
|
40373
|
+
'New value. Comma-separated for arrays; prefix with + to append or - to remove items (e.g. "+spam/repo"). Plain string/number/boolean for scalars.'
|
|
39594
40374
|
)
|
|
39595
40375
|
},
|
|
39596
40376
|
async ({ key, value }) => {
|
|
39597
|
-
|
|
39598
|
-
|
|
39599
|
-
|
|
39600
|
-
|
|
39601
|
-
|
|
39602
|
-
text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`
|
|
39603
|
-
}
|
|
39604
|
-
],
|
|
39605
|
-
isError: true
|
|
39606
|
-
};
|
|
39607
|
-
}
|
|
39608
|
-
let parsed;
|
|
39609
|
-
if (ARRAY_KEYS.has(key)) {
|
|
39610
|
-
try {
|
|
39611
|
-
parsed = JSON.parse(value);
|
|
39612
|
-
if (!Array.isArray(parsed)) {
|
|
39613
|
-
parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39614
|
-
}
|
|
39615
|
-
} catch {
|
|
39616
|
-
parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39617
|
-
}
|
|
39618
|
-
} else if (NUMBER_KEYS.has(key)) {
|
|
39619
|
-
const num = Number(value);
|
|
39620
|
-
if (isNaN(num)) {
|
|
39621
|
-
return {
|
|
39622
|
-
content: [
|
|
39623
|
-
{ type: "text", text: `Invalid number for "${key}": "${value}"` }
|
|
39624
|
-
],
|
|
39625
|
-
isError: true
|
|
39626
|
-
};
|
|
39627
|
-
}
|
|
39628
|
-
parsed = num;
|
|
39629
|
-
} else if (BOOLEAN_KEYS.has(key)) {
|
|
39630
|
-
const lower = value.toLowerCase();
|
|
39631
|
-
if (lower === "true" || lower === "yes") parsed = true;
|
|
39632
|
-
else if (lower === "false" || lower === "no") parsed = false;
|
|
39633
|
-
else {
|
|
39634
|
-
return {
|
|
39635
|
-
content: [
|
|
39636
|
-
{
|
|
39637
|
-
type: "text",
|
|
39638
|
-
text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`
|
|
39639
|
-
}
|
|
39640
|
-
],
|
|
39641
|
-
isError: true
|
|
39642
|
-
};
|
|
39643
|
-
}
|
|
39644
|
-
} else {
|
|
39645
|
-
parsed = value;
|
|
39646
|
-
}
|
|
39647
|
-
const currentPrefs = scout.getPreferences();
|
|
39648
|
-
const candidate = { ...currentPrefs, [key]: parsed };
|
|
39649
|
-
const result = ScoutPreferencesSchema.safeParse(candidate);
|
|
39650
|
-
if (!result.success) {
|
|
39651
|
-
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
40377
|
+
let validated;
|
|
40378
|
+
try {
|
|
40379
|
+
validated = applyPreferenceField(scout.getPreferences(), key, value);
|
|
40380
|
+
} catch (err) {
|
|
40381
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39652
40382
|
return {
|
|
39653
|
-
content: [{ type: "text", text:
|
|
40383
|
+
content: [{ type: "text", text: msg }],
|
|
39654
40384
|
isError: true
|
|
39655
40385
|
};
|
|
39656
40386
|
}
|
|
39657
|
-
scout.updatePreferences({
|
|
39658
|
-
|
|
40387
|
+
scout.updatePreferences({
|
|
40388
|
+
[key]: validated[key]
|
|
40389
|
+
});
|
|
40390
|
+
const persisted = await scout.checkpoint();
|
|
39659
40391
|
const updated = scout.getPreferences();
|
|
39660
40392
|
return {
|
|
39661
|
-
content: [
|
|
40393
|
+
content: [
|
|
40394
|
+
{
|
|
40395
|
+
type: "text",
|
|
40396
|
+
text: JSON.stringify(
|
|
40397
|
+
withPersistWarning(updated, persisted),
|
|
40398
|
+
null,
|
|
40399
|
+
2
|
|
40400
|
+
)
|
|
40401
|
+
}
|
|
40402
|
+
]
|
|
39662
40403
|
};
|
|
39663
40404
|
}
|
|
39664
40405
|
);
|
|
@@ -39714,25 +40455,34 @@ function registerResources(server, scout) {
|
|
|
39714
40455
|
}
|
|
39715
40456
|
|
|
39716
40457
|
// src/index.ts
|
|
39717
|
-
|
|
39718
|
-
|
|
39719
|
-
|
|
39720
|
-
|
|
39721
|
-
|
|
39722
|
-
|
|
39723
|
-
process.exit(1);
|
|
39724
|
-
}
|
|
39725
|
-
const scout = await createScout({ githubToken: token });
|
|
40458
|
+
var SERVER_INFO = {
|
|
40459
|
+
name: "oss-scout-mcp",
|
|
40460
|
+
version: "0.10.0"
|
|
40461
|
+
// x-release-please-version
|
|
40462
|
+
};
|
|
40463
|
+
function createServer(scout) {
|
|
39726
40464
|
const server = new McpServer(
|
|
39727
|
-
{ name:
|
|
40465
|
+
{ name: SERVER_INFO.name, version: SERVER_INFO.version },
|
|
39728
40466
|
{ capabilities: { resources: {}, tools: {} } }
|
|
39729
40467
|
);
|
|
39730
40468
|
registerTools(server, scout);
|
|
39731
40469
|
registerResources(server, scout);
|
|
39732
|
-
|
|
39733
|
-
|
|
40470
|
+
return server;
|
|
40471
|
+
}
|
|
40472
|
+
async function runServer() {
|
|
40473
|
+
const token = getGitHubToken();
|
|
40474
|
+
if (!token) {
|
|
40475
|
+
throw new Error(
|
|
40476
|
+
"GitHub token required. Set GITHUB_TOKEN or run `gh auth login`."
|
|
40477
|
+
);
|
|
40478
|
+
}
|
|
40479
|
+
const scout = await createScout({ githubToken: token, persistence: "local" });
|
|
40480
|
+
const server = createServer(scout);
|
|
40481
|
+
await server.connect(new StdioServerTransport());
|
|
39734
40482
|
}
|
|
39735
|
-
|
|
40483
|
+
|
|
40484
|
+
// src/bin.ts
|
|
40485
|
+
runServer().catch((err) => {
|
|
39736
40486
|
process.stderr.write(
|
|
39737
40487
|
`oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
|
|
39738
40488
|
`
|