@oss-scout/core 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.cjs +57 -51
- package/dist/cli.js +90 -1
- package/dist/commands/config.js +14 -0
- package/dist/commands/features.d.ts +57 -0
- package/dist/commands/features.js +76 -0
- package/dist/commands/search.d.ts +13 -0
- package/dist/commands/search.js +10 -0
- package/dist/core/bootstrap.js +2 -0
- package/dist/core/feature-discovery.d.ts +158 -0
- package/dist/core/feature-discovery.js +380 -0
- package/dist/core/gist-state-store.d.ts +3 -0
- package/dist/core/gist-state-store.js +63 -6
- package/dist/core/issue-discovery.js +2 -0
- package/dist/core/issue-eligibility.js +5 -0
- package/dist/core/issue-scoring.d.ts +16 -0
- package/dist/core/issue-scoring.js +13 -0
- package/dist/core/issue-vetting.d.ts +30 -1
- package/dist/core/issue-vetting.js +29 -3
- package/dist/core/linked-pr.d.ts +18 -0
- package/dist/core/linked-pr.js +25 -0
- package/dist/core/repo-health.js +3 -0
- package/dist/core/roadmap.d.ts +38 -0
- package/dist/core/roadmap.js +131 -0
- package/dist/core/schemas.d.ts +20 -0
- package/dist/core/schemas.js +20 -0
- package/dist/core/search-phases.js +2 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +8 -2
- package/dist/scout.d.ts +25 -2
- package/dist/scout.js +83 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -137,7 +137,10 @@ program
|
|
|
137
137
|
: c.recommendation === "skip"
|
|
138
138
|
? "❌"
|
|
139
139
|
: "⚠️";
|
|
140
|
-
|
|
140
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
141
|
+
? " (stalled PR, revive opportunity)"
|
|
142
|
+
: "";
|
|
143
|
+
console.log(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]${stalledTag}`);
|
|
141
144
|
console.log(` ${c.issue.title}`);
|
|
142
145
|
console.log(` ${c.issue.url}`);
|
|
143
146
|
if (c.repoScore) {
|
|
@@ -154,6 +157,92 @@ program
|
|
|
154
157
|
handleCommandError(err, options);
|
|
155
158
|
}
|
|
156
159
|
});
|
|
160
|
+
program
|
|
161
|
+
.command("features [count]")
|
|
162
|
+
.description("Surface feature-scoped opportunities in repos where you have 3+ merged PRs")
|
|
163
|
+
.option("--json", "Output as JSON")
|
|
164
|
+
.option("--anchor-threshold <n>", "Override featuresAnchorThreshold (1-50)")
|
|
165
|
+
.option("--split-ratio <r>", "Override featuresSplitRatio (0-1, e.g. 0.6)")
|
|
166
|
+
.option("--broad", "Bypass anchor repos; search feature issues across the ecosystem (first-touch mode)")
|
|
167
|
+
.action(async (count, options) => {
|
|
168
|
+
try {
|
|
169
|
+
const { runFeatures } = await import("./commands/features.js");
|
|
170
|
+
const maxResults = count ? parseInt(count, 10) : 10;
|
|
171
|
+
if (isNaN(maxResults) || maxResults < 1 || maxResults > 50) {
|
|
172
|
+
console.error("Error: count must be an integer between 1 and 50");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
let anchorThreshold;
|
|
176
|
+
if (options.anchorThreshold !== undefined) {
|
|
177
|
+
const parsed = parseInt(options.anchorThreshold, 10);
|
|
178
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 50) {
|
|
179
|
+
console.error("Error: --anchor-threshold must be an integer between 1 and 50");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
anchorThreshold = parsed;
|
|
183
|
+
}
|
|
184
|
+
let splitRatio;
|
|
185
|
+
if (options.splitRatio !== undefined) {
|
|
186
|
+
const parsed = Number.parseFloat(options.splitRatio);
|
|
187
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 1) {
|
|
188
|
+
console.error("Error: --split-ratio must be a number between 0 and 1");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
splitRatio = parsed;
|
|
192
|
+
}
|
|
193
|
+
const state = loadLocalState();
|
|
194
|
+
const result = await runFeatures({
|
|
195
|
+
maxResults,
|
|
196
|
+
state,
|
|
197
|
+
anchorThreshold,
|
|
198
|
+
splitRatio,
|
|
199
|
+
broad: options.broad,
|
|
200
|
+
});
|
|
201
|
+
if (options.json) {
|
|
202
|
+
console.log(formatJsonSuccess(result));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const total = result.quickWins.length + result.biggerBets.length;
|
|
206
|
+
if (result.message) {
|
|
207
|
+
console.log(`\n${result.message}\n`);
|
|
208
|
+
}
|
|
209
|
+
if (total === 0)
|
|
210
|
+
return;
|
|
211
|
+
const headerScope = options.broad
|
|
212
|
+
? "across the ecosystem"
|
|
213
|
+
: "in your anchor repos";
|
|
214
|
+
console.log(`\n🎯 Feature opportunities ${headerScope} (${result.quickWins.length} quick wins + ${result.biggerBets.length} bigger bets)\n`);
|
|
215
|
+
if (!options.broad) {
|
|
216
|
+
console.log(`Anchor repos: ${result.anchorRepos.join(", ")}\n`);
|
|
217
|
+
}
|
|
218
|
+
if (result.quickWins.length) {
|
|
219
|
+
console.log("── Quick wins ─────────────────────────────────────────");
|
|
220
|
+
for (const c of result.quickWins) {
|
|
221
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
222
|
+
? " (stalled PR, revive opportunity)"
|
|
223
|
+
: "";
|
|
224
|
+
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
225
|
+
console.log(` ${c.issue.url}`);
|
|
226
|
+
}
|
|
227
|
+
console.log("");
|
|
228
|
+
}
|
|
229
|
+
if (result.biggerBets.length) {
|
|
230
|
+
console.log("── Bigger bets ────────────────────────────────────────");
|
|
231
|
+
for (const c of result.biggerBets) {
|
|
232
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
233
|
+
? " (stalled PR, revive opportunity)"
|
|
234
|
+
: "";
|
|
235
|
+
console.log(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
236
|
+
console.log(` ${c.issue.url}`);
|
|
237
|
+
}
|
|
238
|
+
console.log("");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
handleCommandError(err, options);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
157
246
|
// ── results command ────────────────────────────────────────────────
|
|
158
247
|
const resultsCmd = program
|
|
159
248
|
.command("results")
|
package/dist/commands/config.js
CHANGED
|
@@ -28,6 +28,8 @@ const FIELD_CONFIGS = {
|
|
|
28
28
|
githubUsername: { type: "string" },
|
|
29
29
|
broadPhaseDelayMs: { type: "number" },
|
|
30
30
|
skipBroadWhenSufficientResults: { type: "number" },
|
|
31
|
+
featuresAnchorThreshold: { type: "number" },
|
|
32
|
+
featuresSplitRatio: { type: "float" },
|
|
31
33
|
};
|
|
32
34
|
function parseBoolean(value) {
|
|
33
35
|
const lower = value.toLowerCase();
|
|
@@ -44,6 +46,13 @@ function parseNumber(value, key) {
|
|
|
44
46
|
}
|
|
45
47
|
return num;
|
|
46
48
|
}
|
|
49
|
+
function parseFloat(value, key) {
|
|
50
|
+
const num = Number.parseFloat(value);
|
|
51
|
+
if (isNaN(num)) {
|
|
52
|
+
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
53
|
+
}
|
|
54
|
+
return num;
|
|
55
|
+
}
|
|
47
56
|
function parseArrayValue(value) {
|
|
48
57
|
return value
|
|
49
58
|
.split(",")
|
|
@@ -96,6 +105,8 @@ export function runConfigShow() {
|
|
|
96
105
|
console.log(` persistence: ${prefs.persistence}`);
|
|
97
106
|
console.log(` broadPhaseDelayMs: ${prefs.broadPhaseDelayMs}ms (${(prefs.broadPhaseDelayMs / 1000).toFixed(0)}s)`);
|
|
98
107
|
console.log(` skipBroadWhenSufficientResults: ${prefs.skipBroadWhenSufficientResults}`);
|
|
108
|
+
console.log(` featuresAnchorThreshold: ${prefs.featuresAnchorThreshold}`);
|
|
109
|
+
console.log(` featuresSplitRatio: ${prefs.featuresSplitRatio}`);
|
|
99
110
|
console.log();
|
|
100
111
|
}
|
|
101
112
|
/**
|
|
@@ -125,6 +136,9 @@ export function runConfigSet(key, value) {
|
|
|
125
136
|
case "number":
|
|
126
137
|
prefs[key] = parseNumber(value, key);
|
|
127
138
|
break;
|
|
139
|
+
case "float":
|
|
140
|
+
prefs[key] = parseFloat(value, key);
|
|
141
|
+
break;
|
|
128
142
|
case "array": {
|
|
129
143
|
const current = prefs[key] ?? [];
|
|
130
144
|
prefs[key] = updateArray(current, value);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features command — surfaces feature opportunities in anchor repos.
|
|
3
|
+
*/
|
|
4
|
+
import type { ScoutState } from "../core/schemas.js";
|
|
5
|
+
/**
|
|
6
|
+
* Linked-PR metadata surfaced on feature candidates. `isStalled` flags open
|
|
7
|
+
* PRs that haven't been updated for 30+ days — surfaced as revive
|
|
8
|
+
* opportunities (#97). Scoring is unchanged: the existing -30 viability
|
|
9
|
+
* penalty still applies.
|
|
10
|
+
*/
|
|
11
|
+
interface OutputLinkedPR {
|
|
12
|
+
number: number;
|
|
13
|
+
state: "open" | "closed";
|
|
14
|
+
url: string;
|
|
15
|
+
updatedAt?: string;
|
|
16
|
+
isStalled: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface FeaturesOutput {
|
|
19
|
+
quickWins: Array<{
|
|
20
|
+
issue: {
|
|
21
|
+
repo: string;
|
|
22
|
+
number: number;
|
|
23
|
+
title: string;
|
|
24
|
+
url: string;
|
|
25
|
+
labels: string[];
|
|
26
|
+
};
|
|
27
|
+
recommendation: "approve" | "skip" | "needs_review";
|
|
28
|
+
viabilityScore: number;
|
|
29
|
+
horizon: "quick-win";
|
|
30
|
+
linkedPR?: OutputLinkedPR;
|
|
31
|
+
}>;
|
|
32
|
+
biggerBets: Array<{
|
|
33
|
+
issue: {
|
|
34
|
+
repo: string;
|
|
35
|
+
number: number;
|
|
36
|
+
title: string;
|
|
37
|
+
url: string;
|
|
38
|
+
labels: string[];
|
|
39
|
+
};
|
|
40
|
+
recommendation: "approve" | "skip" | "needs_review";
|
|
41
|
+
viabilityScore: number;
|
|
42
|
+
horizon: "bigger-bet";
|
|
43
|
+
linkedPR?: OutputLinkedPR;
|
|
44
|
+
}>;
|
|
45
|
+
anchorRepos: string[];
|
|
46
|
+
message: string | null;
|
|
47
|
+
}
|
|
48
|
+
interface FeaturesCommandOptions {
|
|
49
|
+
maxResults: number;
|
|
50
|
+
state?: ScoutState;
|
|
51
|
+
anchorThreshold?: number;
|
|
52
|
+
splitRatio?: number;
|
|
53
|
+
/** Run the broad / cross-repo path (#100). */
|
|
54
|
+
broad?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare function runFeatures(options: FeaturesCommandOptions): Promise<FeaturesOutput>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features command — surfaces feature opportunities in anchor repos.
|
|
3
|
+
*/
|
|
4
|
+
import { createScout } from "../scout.js";
|
|
5
|
+
import { requireGitHubToken } from "../core/utils.js";
|
|
6
|
+
import { saveLocalState } from "../core/local-state.js";
|
|
7
|
+
import { isLinkedPRStalled } from "../core/linked-pr.js";
|
|
8
|
+
function mapLinkedPR(c) {
|
|
9
|
+
const linkedPR = c.vettingResult.linkedPR;
|
|
10
|
+
if (!linkedPR)
|
|
11
|
+
return undefined;
|
|
12
|
+
return {
|
|
13
|
+
number: linkedPR.number,
|
|
14
|
+
state: linkedPR.state,
|
|
15
|
+
url: linkedPR.url,
|
|
16
|
+
updatedAt: linkedPR.updatedAt,
|
|
17
|
+
isStalled: isLinkedPRStalled(linkedPR),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function mapQuickWin(c) {
|
|
21
|
+
return {
|
|
22
|
+
issue: {
|
|
23
|
+
repo: c.issue.repo,
|
|
24
|
+
number: c.issue.number,
|
|
25
|
+
title: c.issue.title,
|
|
26
|
+
url: c.issue.url,
|
|
27
|
+
labels: c.issue.labels,
|
|
28
|
+
},
|
|
29
|
+
recommendation: c.recommendation,
|
|
30
|
+
viabilityScore: c.viabilityScore,
|
|
31
|
+
horizon: "quick-win",
|
|
32
|
+
linkedPR: mapLinkedPR(c),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function mapBiggerBet(c) {
|
|
36
|
+
return {
|
|
37
|
+
issue: {
|
|
38
|
+
repo: c.issue.repo,
|
|
39
|
+
number: c.issue.number,
|
|
40
|
+
title: c.issue.title,
|
|
41
|
+
url: c.issue.url,
|
|
42
|
+
labels: c.issue.labels,
|
|
43
|
+
},
|
|
44
|
+
recommendation: c.recommendation,
|
|
45
|
+
viabilityScore: c.viabilityScore,
|
|
46
|
+
horizon: "bigger-bet",
|
|
47
|
+
linkedPR: mapLinkedPR(c),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export async function runFeatures(options) {
|
|
51
|
+
const token = requireGitHubToken();
|
|
52
|
+
const scout = options.state
|
|
53
|
+
? await createScout({
|
|
54
|
+
githubToken: token,
|
|
55
|
+
persistence: "provided",
|
|
56
|
+
initialState: options.state,
|
|
57
|
+
})
|
|
58
|
+
: await createScout({ githubToken: token });
|
|
59
|
+
const result = await scout.features({
|
|
60
|
+
count: options.maxResults,
|
|
61
|
+
anchorThreshold: options.anchorThreshold,
|
|
62
|
+
splitRatio: options.splitRatio,
|
|
63
|
+
broad: options.broad,
|
|
64
|
+
});
|
|
65
|
+
saveLocalState(scout.getState());
|
|
66
|
+
const persisted = await scout.checkpoint();
|
|
67
|
+
if (!persisted) {
|
|
68
|
+
console.error("Warning: changes saved locally but gist sync failed.");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
quickWins: result.quickWins.map(mapQuickWin),
|
|
72
|
+
biggerBets: result.biggerBets.map(mapBiggerBet),
|
|
73
|
+
anchorRepos: result.anchorRepos,
|
|
74
|
+
message: result.message,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -24,6 +24,19 @@ export interface SearchOutput {
|
|
|
24
24
|
isResponsive: boolean;
|
|
25
25
|
lastMergedAt?: string;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Metadata for the first cross-referenced PR linked to this issue, when
|
|
29
|
+
* one exists. `isStalled` flags open PRs that haven't been updated for
|
|
30
|
+
* 30+ days — surfaced as revive opportunities (#97). Scoring is
|
|
31
|
+
* unchanged: the existing -30 viability penalty still applies.
|
|
32
|
+
*/
|
|
33
|
+
linkedPR?: {
|
|
34
|
+
number: number;
|
|
35
|
+
state: "open" | "closed";
|
|
36
|
+
url: string;
|
|
37
|
+
updatedAt?: string;
|
|
38
|
+
isStalled: boolean;
|
|
39
|
+
};
|
|
27
40
|
}>;
|
|
28
41
|
excludedRepos: string[];
|
|
29
42
|
aiPolicyBlocklist: string[];
|
package/dist/commands/search.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { createScout } from "../scout.js";
|
|
5
5
|
import { requireGitHubToken } from "../core/utils.js";
|
|
6
6
|
import { saveLocalState } from "../core/local-state.js";
|
|
7
|
+
import { isLinkedPRStalled } from "../core/linked-pr.js";
|
|
7
8
|
export async function runSearch(options) {
|
|
8
9
|
const token = requireGitHubToken();
|
|
9
10
|
const scout = options.state
|
|
@@ -50,6 +51,15 @@ export async function runSearch(options) {
|
|
|
50
51
|
lastMergedAt: repoScoreRecord.lastMergedAt,
|
|
51
52
|
}
|
|
52
53
|
: undefined,
|
|
54
|
+
linkedPR: c.vettingResult.linkedPR
|
|
55
|
+
? {
|
|
56
|
+
number: c.vettingResult.linkedPR.number,
|
|
57
|
+
state: c.vettingResult.linkedPR.state,
|
|
58
|
+
url: c.vettingResult.linkedPR.url,
|
|
59
|
+
updatedAt: c.vettingResult.linkedPR.updatedAt,
|
|
60
|
+
isStalled: isLinkedPRStalled(c.vettingResult.linkedPR),
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
53
63
|
};
|
|
54
64
|
}),
|
|
55
65
|
excludedRepos: result.excludedRepos,
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -51,6 +51,8 @@ export async function bootstrapScout(scout, token) {
|
|
|
51
51
|
scout.setStarredRepos(starredRepos);
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
55
|
+
throw err;
|
|
54
56
|
warn(MODULE, `Failed to fetch starred repos: ${errorMessage(err)}`);
|
|
55
57
|
errors.push("starred repos fetch failed");
|
|
56
58
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Discovery — orchestrates `scout features` mode: surfaces
|
|
3
|
+
* feature-scoped contribution opportunities in repos where the user has
|
|
4
|
+
* 3+ merged PRs, ranked into separate "quick wins" and "bigger bets" buckets.
|
|
5
|
+
*
|
|
6
|
+
* Reuses existing infrastructure:
|
|
7
|
+
* - issue-vetting.ts — per-issue vetting + scoring (with featureSignals)
|
|
8
|
+
* - issue-scoring.ts — viability score (existing weights + feature bonuses)
|
|
9
|
+
* - http-cache.ts — response cache
|
|
10
|
+
* - errors.ts — auth/rate-limit propagation
|
|
11
|
+
*
|
|
12
|
+
* No state singletons — anchor repos are resolved from RepoScore[] passed in.
|
|
13
|
+
*/
|
|
14
|
+
import type { Octokit } from "@octokit/rest";
|
|
15
|
+
import type { RepoScore, Horizon } from "./schemas.js";
|
|
16
|
+
import type { IssueCandidate } from "./types.js";
|
|
17
|
+
import type { IssueVetter } from "./issue-vetting.js";
|
|
18
|
+
/** Default minimum merged-PR count for a repo to qualify as an anchor. */
|
|
19
|
+
export declare const ANCHOR_THRESHOLD = 3;
|
|
20
|
+
/** Default quick-wins / bigger-bets split ratio (60/40). */
|
|
21
|
+
export declare const DEFAULT_SPLIT_RATIO = 0.6;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve anchor repos: those with mergedPRCount >= threshold (default 3),
|
|
24
|
+
* sorted by mergedPRCount descending. ScoutState stores repoScores as a
|
|
25
|
+
* Record<string, RepoScore>, so we read its values.
|
|
26
|
+
*
|
|
27
|
+
* @param threshold Override minimum merged-PR count (#98).
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveAnchorRepos(repoScores: Record<string, RepoScore>, threshold?: number): string[];
|
|
30
|
+
/** Labels that promote an issue to the "bigger-bet" bucket. */
|
|
31
|
+
export declare const BIGGER_BET_LABELS: Set<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Classify an issue into "quick-win" or "bigger-bet" based on
|
|
34
|
+
* maintainer-commitment signals (milestone presence, label set, ROADMAP.md
|
|
35
|
+
* membership). Roadmap membership (#95) is treated as an explicit
|
|
36
|
+
* maintainer commitment and forces the bigger-bet horizon.
|
|
37
|
+
*/
|
|
38
|
+
export declare function classifyHorizon(input: {
|
|
39
|
+
hasMilestone: boolean;
|
|
40
|
+
labels: string[];
|
|
41
|
+
isOnRoadmap?: boolean;
|
|
42
|
+
}): Horizon;
|
|
43
|
+
/** A vetted issue candidate stamped with its horizon classification. */
|
|
44
|
+
export type FeatureCandidate = IssueCandidate & {
|
|
45
|
+
horizon: Horizon;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Split feature candidates into two buckets respecting a configurable
|
|
49
|
+
* quick-wins / bigger-bets ratio (default 60/40). If either bucket is
|
|
50
|
+
* short, redirect the deficit to the other bucket. Each bucket is
|
|
51
|
+
* sorted by viabilityScore descending.
|
|
52
|
+
*
|
|
53
|
+
* @param ratio Fraction (0..1) of `count` to allocate to quick wins (#99).
|
|
54
|
+
*/
|
|
55
|
+
export declare function splitByHorizon(candidates: FeatureCandidate[], count: number, ratio?: number): {
|
|
56
|
+
quickWins: FeatureCandidate[];
|
|
57
|
+
biggerBets: FeatureCandidate[];
|
|
58
|
+
};
|
|
59
|
+
/** Feature labels used to filter issues. Any-of match. */
|
|
60
|
+
export declare const FEATURE_LABELS: readonly ["enhancement", "feature", "feature-request", "proposal", "roadmap", "accepted-rfc"];
|
|
61
|
+
/** Labels excluded from feature-mode results (overlap with `scout` territory). */
|
|
62
|
+
export declare const FEATURE_EXCLUSION_LABELS: Set<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Labels that signal "the maintainer wants outside contributions". When any
|
|
65
|
+
* is present, combined with no linked PR and an issue age >= 60 days, the
|
|
66
|
+
* issue is treated as wontfix-no-contributor (#96).
|
|
67
|
+
*/
|
|
68
|
+
export declare const WONTFIX_NO_CONTRIBUTOR_LABELS: Set<string>;
|
|
69
|
+
/** Minimum days an issue must be open to qualify as wontfix-no-contributor. */
|
|
70
|
+
export declare const WONTFIX_MIN_AGE_DAYS = 60;
|
|
71
|
+
/**
|
|
72
|
+
* Pure detector for the "wontfix because no contributor stepped up" pattern (#96).
|
|
73
|
+
*
|
|
74
|
+
* True when:
|
|
75
|
+
* - issue carries any of WONTFIX_NO_CONTRIBUTOR_LABELS, AND
|
|
76
|
+
* - issue has been open at least `minAgeDays` days (default 60)
|
|
77
|
+
*
|
|
78
|
+
* The orchestrator already filters out assigned issues before reaching the
|
|
79
|
+
* vetter. Linked-PR cases are deliberately not gated here: the existing
|
|
80
|
+
* -30 viability penalty for `hasExistingPR` already discounts those, and
|
|
81
|
+
* checking `hasLinkedPR` would require deferring scoring until after vet,
|
|
82
|
+
* doubling the work for a marginally cleaner signal.
|
|
83
|
+
*/
|
|
84
|
+
export declare function detectWontfixNoContributor(input: {
|
|
85
|
+
labels: string[];
|
|
86
|
+
createdAt: string;
|
|
87
|
+
now?: Date;
|
|
88
|
+
minAgeDays?: number;
|
|
89
|
+
}): boolean;
|
|
90
|
+
export declare const NO_ANCHORS_MESSAGE = "No anchor repos yet (need 3+ merged PRs in a repo). Try `scout search` to build relationships first.";
|
|
91
|
+
export declare const NO_RESULTS_MESSAGE = "No open feature opportunities in your anchor repos right now. Check back next week, or try `scout search` for fix-mode work.";
|
|
92
|
+
export interface FeatureSearchResult {
|
|
93
|
+
quickWins: FeatureCandidate[];
|
|
94
|
+
biggerBets: FeatureCandidate[];
|
|
95
|
+
anchorRepos: string[];
|
|
96
|
+
message: string | null;
|
|
97
|
+
}
|
|
98
|
+
export interface DiscoverFeaturesOptions {
|
|
99
|
+
octokit: Octokit;
|
|
100
|
+
vetter: IssueVetter;
|
|
101
|
+
repoScores: Record<string, RepoScore>;
|
|
102
|
+
count: number;
|
|
103
|
+
/** Override default anchor threshold (3 merged PRs). */
|
|
104
|
+
anchorThreshold?: number;
|
|
105
|
+
/** Override default split ratio (0.6 = 60% quick wins, 40% bigger bets). */
|
|
106
|
+
splitRatio?: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Orchestrate `scout features`: anchor resolution → per-repo issue listing
|
|
110
|
+
* → feature-signal extraction → vetting → horizon classification → bucket split.
|
|
111
|
+
*
|
|
112
|
+
* Returns separate "quick wins" and "bigger bets" buckets per the 60/40 target,
|
|
113
|
+
* with a human-friendly message when no anchors qualify or no candidates pass
|
|
114
|
+
* the viability threshold.
|
|
115
|
+
*
|
|
116
|
+
* Auth (401) and rate-limit errors propagate. Per-repo and per-issue failures
|
|
117
|
+
* degrade gracefully via `warn`.
|
|
118
|
+
*/
|
|
119
|
+
export declare function discoverFeatures(opts: DiscoverFeaturesOptions): Promise<FeatureSearchResult>;
|
|
120
|
+
export declare const NO_BROAD_RESULTS_MESSAGE = "No open feature opportunities matched your filters. Try widening your language preferences in `scout config`.";
|
|
121
|
+
export interface DiscoverFeaturesBroadOptions {
|
|
122
|
+
octokit: Octokit;
|
|
123
|
+
vetter: IssueVetter;
|
|
124
|
+
count: number;
|
|
125
|
+
/** Languages from user preferences ("any" disables the filter). */
|
|
126
|
+
languages?: string[];
|
|
127
|
+
excludeRepos?: string[];
|
|
128
|
+
excludeOrgs?: string[];
|
|
129
|
+
/** Override default split ratio. */
|
|
130
|
+
splitRatio?: number;
|
|
131
|
+
/** Maximum search results to vet (default 30). */
|
|
132
|
+
maxToVet?: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build a GitHub Search query for cross-repo feature discovery.
|
|
136
|
+
*
|
|
137
|
+
* Exported separately from `discoverFeaturesBroad` so the query construction
|
|
138
|
+
* is independently testable without mocking the Search API.
|
|
139
|
+
*/
|
|
140
|
+
export declare function buildBroadFeatureSearchQuery(opts: {
|
|
141
|
+
languages?: string[];
|
|
142
|
+
excludeRepos?: string[];
|
|
143
|
+
excludeOrgs?: string[];
|
|
144
|
+
}): string;
|
|
145
|
+
/**
|
|
146
|
+
* Orchestrate broad / cross-repo feature discovery (#100). Bypasses anchor
|
|
147
|
+
* resolution; runs a single GitHub Search API query for feature-labeled
|
|
148
|
+
* open issues across the entire ecosystem, filtered by the user's language
|
|
149
|
+
* preferences and excluded repos/orgs.
|
|
150
|
+
*
|
|
151
|
+
* Designed for first-touch contributors who haven't yet built repo
|
|
152
|
+
* relationships and so wouldn't qualify under the default `scout features`
|
|
153
|
+
* anchor-based path.
|
|
154
|
+
*
|
|
155
|
+
* Auth (401) and rate-limit errors propagate; per-issue vet failures
|
|
156
|
+
* degrade gracefully.
|
|
157
|
+
*/
|
|
158
|
+
export declare function discoverFeaturesBroad(opts: DiscoverFeaturesBroadOptions): Promise<FeatureSearchResult>;
|