@oss-scout/core 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.cjs +49 -43
- 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/feature-discovery.d.ts +158 -0
- package/dist/core/feature-discovery.js +380 -0
- package/dist/core/issue-discovery.js +4 -7
- 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 +11 -2
- package/dist/core/linked-pr.d.ts +18 -0
- package/dist/core/linked-pr.js +25 -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.d.ts +24 -0
- package/dist/core/search-phases.js +50 -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 +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roadmap scraping (#95) — fetches a repo's ROADMAP.md (or variant) and
|
|
3
|
+
* extracts referenced GitHub issue numbers. Used by feature-discovery to
|
|
4
|
+
* apply an `onRoadmap` bonus when the maintainer has publicly committed
|
|
5
|
+
* to an issue in their roadmap doc.
|
|
6
|
+
*
|
|
7
|
+
* Cached for 1 hour per repo. 404s are cached as empty sets so repos
|
|
8
|
+
* without a roadmap don't get re-probed every run.
|
|
9
|
+
*
|
|
10
|
+
* Auth (401) and rate-limit errors propagate, matching the rest of the
|
|
11
|
+
* codebase's error strategy. Other errors degrade gracefully (warn + empty).
|
|
12
|
+
*/
|
|
13
|
+
import { errorMessage, getHttpStatusCode, isRateLimitError } from "./errors.js";
|
|
14
|
+
import { warn } from "./logger.js";
|
|
15
|
+
const MODULE = "roadmap";
|
|
16
|
+
/** TTL for roadmap fetch results (1 hour). */
|
|
17
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
18
|
+
/** Paths probed in priority order. First success wins. */
|
|
19
|
+
const ROADMAP_PATHS = [
|
|
20
|
+
"ROADMAP.md",
|
|
21
|
+
"docs/ROADMAP.md",
|
|
22
|
+
".github/ROADMAP.md",
|
|
23
|
+
"Roadmap.md",
|
|
24
|
+
"roadmap.md",
|
|
25
|
+
];
|
|
26
|
+
const roadmapCache = new Map();
|
|
27
|
+
/** Drop expired entries. Called from each fetch. */
|
|
28
|
+
function pruneCache() {
|
|
29
|
+
const cutoff = Date.now() - CACHE_TTL_MS;
|
|
30
|
+
for (const [key, value] of roadmapCache.entries()) {
|
|
31
|
+
if (value.fetchedAt < cutoff)
|
|
32
|
+
roadmapCache.delete(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse markdown content for issue number references.
|
|
37
|
+
*
|
|
38
|
+
* Extracts:
|
|
39
|
+
* - bare `#NNN` references that are not on a markdown heading line
|
|
40
|
+
* - `<owner>/<repo>#NNN` cross-repo refs that match the repo we're parsing for
|
|
41
|
+
* - `https://github.com/<owner>/<repo>/issues/NNN` URLs that match the
|
|
42
|
+
* repo we're parsing for (URLs to other repos are ignored)
|
|
43
|
+
*
|
|
44
|
+
* The match for cross-repo `owner/repo#N` and full URLs is intentional:
|
|
45
|
+
* scattered references to unrelated repos in a roadmap shouldn't bump
|
|
46
|
+
* scores in those repos.
|
|
47
|
+
*/
|
|
48
|
+
export function parseRoadmapIssueRefs(content, owner, repo) {
|
|
49
|
+
const refs = new Set();
|
|
50
|
+
const ownerRepoLower = `${owner}/${repo}`.toLowerCase();
|
|
51
|
+
// `owner/repo#N` cross-repo style refs — must match the current repo.
|
|
52
|
+
// We escape regex metachars in case repo names contain `.` or `-`.
|
|
53
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
const fullRefPattern = new RegExp(`\\b${escape(owner)}/${escape(repo)}#(\\d+)\\b`, "gi");
|
|
55
|
+
for (const m of content.matchAll(fullRefPattern)) {
|
|
56
|
+
const n = Number.parseInt(m[1], 10);
|
|
57
|
+
if (n > 0)
|
|
58
|
+
refs.add(n);
|
|
59
|
+
}
|
|
60
|
+
// Bare `#N` references, scanned line-by-line so we can skip markdown
|
|
61
|
+
// headings (`# title`, `## section`) — those aren't issue refs. We also
|
|
62
|
+
// strip out `owner/repo#N` matches first so `#N` in a cross-repo ref to
|
|
63
|
+
// a different repo doesn't leak in as a bare match.
|
|
64
|
+
const fullRefStripPattern = /\b[\w.-]+\/[\w.-]+#\d+\b/gi;
|
|
65
|
+
for (const line of content.split("\n")) {
|
|
66
|
+
if (/^\s*#+\s/.test(line))
|
|
67
|
+
continue;
|
|
68
|
+
const stripped = line.replace(fullRefStripPattern, "");
|
|
69
|
+
for (const m of stripped.matchAll(/(?:^|[^&\w])#(\d+)\b/g)) {
|
|
70
|
+
const n = Number.parseInt(m[1], 10);
|
|
71
|
+
if (n > 0)
|
|
72
|
+
refs.add(n);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Full GitHub issue URLs scoped to this repo.
|
|
76
|
+
const urlPattern = /https?:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)/gi;
|
|
77
|
+
for (const m of content.matchAll(urlPattern)) {
|
|
78
|
+
if (m[1].toLowerCase() === ownerRepoLower) {
|
|
79
|
+
const n = Number.parseInt(m[2], 10);
|
|
80
|
+
if (n > 0)
|
|
81
|
+
refs.add(n);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return refs;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch and parse the repo's ROADMAP.md, returning the set of referenced
|
|
88
|
+
* issue numbers. Returns an empty set if no roadmap is found.
|
|
89
|
+
*
|
|
90
|
+
* Probes ROADMAP_PATHS sequentially until a 200 response is received.
|
|
91
|
+
* Auth/rate-limit errors propagate; other errors are logged and degrade
|
|
92
|
+
* to an empty set.
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
95
|
+
const cacheKey = `${owner}/${repo}`.toLowerCase();
|
|
96
|
+
const cached = roadmapCache.get(cacheKey);
|
|
97
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
98
|
+
return cached.refs;
|
|
99
|
+
}
|
|
100
|
+
for (const path of ROADMAP_PATHS) {
|
|
101
|
+
try {
|
|
102
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path });
|
|
103
|
+
if (!("content" in data))
|
|
104
|
+
continue;
|
|
105
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
106
|
+
const refs = parseRoadmapIssueRefs(content, owner, repo);
|
|
107
|
+
roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
|
|
108
|
+
pruneCache();
|
|
109
|
+
return refs;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
113
|
+
throw err;
|
|
114
|
+
const status = getHttpStatusCode(err);
|
|
115
|
+
if (status === 404)
|
|
116
|
+
continue; // path missing — try next
|
|
117
|
+
warn(MODULE, `Unexpected error fetching ${path} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
118
|
+
// Fall through and try next path.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// No roadmap found (or all probes errored softly). Cache the empty result
|
|
122
|
+
// so we don't re-probe every run.
|
|
123
|
+
const empty = new Set();
|
|
124
|
+
roadmapCache.set(cacheKey, { refs: empty, fetchedAt: Date.now() });
|
|
125
|
+
pruneCache();
|
|
126
|
+
return empty;
|
|
127
|
+
}
|
|
128
|
+
/** Test-only: clear the in-memory cache. */
|
|
129
|
+
export function _clearRoadmapCacheForTests() {
|
|
130
|
+
roadmapCache.clear();
|
|
131
|
+
}
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ export declare const LinkedPRSchema: z.ZodObject<{
|
|
|
94
94
|
}>;
|
|
95
95
|
merged: z.ZodBoolean;
|
|
96
96
|
url: z.ZodString;
|
|
97
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
97
98
|
}, z.core.$strip>;
|
|
98
99
|
export declare const IssueVettingResultSchema: z.ZodObject<{
|
|
99
100
|
passedAllChecks: z.ZodBoolean;
|
|
@@ -129,6 +130,7 @@ export declare const IssueVettingResultSchema: z.ZodObject<{
|
|
|
129
130
|
}>;
|
|
130
131
|
merged: z.ZodBoolean;
|
|
131
132
|
url: z.ZodString;
|
|
133
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
132
134
|
}, z.core.$strip>>>;
|
|
133
135
|
notes: z.ZodArray<z.ZodString>;
|
|
134
136
|
}, z.core.$strip>;
|
|
@@ -182,6 +184,7 @@ export declare const TrackedIssueSchema: z.ZodObject<{
|
|
|
182
184
|
}>;
|
|
183
185
|
merged: z.ZodBoolean;
|
|
184
186
|
url: z.ZodString;
|
|
187
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
185
188
|
}, z.core.$strip>>>;
|
|
186
189
|
notes: z.ZodArray<z.ZodString>;
|
|
187
190
|
}, z.core.$strip>>;
|
|
@@ -193,6 +196,10 @@ export declare const SkippedIssueSchema: z.ZodObject<{
|
|
|
193
196
|
title: z.ZodString;
|
|
194
197
|
skippedAt: z.ZodString;
|
|
195
198
|
}, z.core.$strip>;
|
|
199
|
+
export declare const HorizonSchema: z.ZodEnum<{
|
|
200
|
+
"quick-win": "quick-win";
|
|
201
|
+
"bigger-bet": "bigger-bet";
|
|
202
|
+
}>;
|
|
196
203
|
export declare const SavedCandidateSchema: z.ZodObject<{
|
|
197
204
|
issueUrl: z.ZodString;
|
|
198
205
|
repo: z.ZodString;
|
|
@@ -209,6 +216,10 @@ export declare const SavedCandidateSchema: z.ZodObject<{
|
|
|
209
216
|
firstSeenAt: z.ZodString;
|
|
210
217
|
lastSeenAt: z.ZodString;
|
|
211
218
|
lastScore: z.ZodNumber;
|
|
219
|
+
horizon: z.ZodOptional<z.ZodEnum<{
|
|
220
|
+
"quick-win": "quick-win";
|
|
221
|
+
"bigger-bet": "bigger-bet";
|
|
222
|
+
}>>;
|
|
212
223
|
}, z.core.$strip>;
|
|
213
224
|
export declare const PersistenceModeSchema: z.ZodEnum<{
|
|
214
225
|
local: "local";
|
|
@@ -254,6 +265,8 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
254
265
|
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
255
266
|
slmTriageModel: z.ZodDefault<z.ZodString>;
|
|
256
267
|
slmTriageHost: z.ZodDefault<z.ZodString>;
|
|
268
|
+
featuresAnchorThreshold: z.ZodDefault<z.ZodNumber>;
|
|
269
|
+
featuresSplitRatio: z.ZodDefault<z.ZodNumber>;
|
|
257
270
|
}, z.core.$strip>;
|
|
258
271
|
export declare const ScoutStateSchema: z.ZodObject<{
|
|
259
272
|
version: z.ZodLiteral<1>;
|
|
@@ -297,6 +310,8 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
297
310
|
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
298
311
|
slmTriageModel: z.ZodDefault<z.ZodString>;
|
|
299
312
|
slmTriageHost: z.ZodDefault<z.ZodString>;
|
|
313
|
+
featuresAnchorThreshold: z.ZodDefault<z.ZodNumber>;
|
|
314
|
+
featuresSplitRatio: z.ZodDefault<z.ZodNumber>;
|
|
300
315
|
}, z.core.$strip>>;
|
|
301
316
|
repoScores: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
302
317
|
repo: z.ZodString;
|
|
@@ -347,6 +362,10 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
347
362
|
firstSeenAt: z.ZodString;
|
|
348
363
|
lastSeenAt: z.ZodString;
|
|
349
364
|
lastScore: z.ZodNumber;
|
|
365
|
+
horizon: z.ZodOptional<z.ZodEnum<{
|
|
366
|
+
"quick-win": "quick-win";
|
|
367
|
+
"bigger-bet": "bigger-bet";
|
|
368
|
+
}>>;
|
|
350
369
|
}, z.core.$strip>>>;
|
|
351
370
|
skippedIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
352
371
|
url: z.ZodString;
|
|
@@ -372,6 +391,7 @@ export type LinkedPR = z.infer<typeof LinkedPRSchema>;
|
|
|
372
391
|
export type IssueVettingResult = z.infer<typeof IssueVettingResultSchema>;
|
|
373
392
|
export type TrackedIssue = z.infer<typeof TrackedIssueSchema>;
|
|
374
393
|
export type ScoutPreferences = z.infer<typeof ScoutPreferencesSchema>;
|
|
394
|
+
export type Horizon = z.infer<typeof HorizonSchema>;
|
|
375
395
|
export type SavedCandidate = z.infer<typeof SavedCandidateSchema>;
|
|
376
396
|
export type SkippedIssue = z.infer<typeof SkippedIssueSchema>;
|
|
377
397
|
export type ScoutState = z.infer<typeof ScoutStateSchema>;
|
package/dist/core/schemas.js
CHANGED
|
@@ -95,6 +95,12 @@ export const LinkedPRSchema = z.object({
|
|
|
95
95
|
state: z.enum(["open", "closed"]),
|
|
96
96
|
merged: z.boolean(),
|
|
97
97
|
url: z.string(),
|
|
98
|
+
/**
|
|
99
|
+
* ISO-8601 timestamp of the linked PR's last update. Optional so existing
|
|
100
|
+
* persisted state validates unchanged. Used by `isLinkedPRStalled` to
|
|
101
|
+
* surface stale revive opportunities (#97).
|
|
102
|
+
*/
|
|
103
|
+
updatedAt: z.string().optional(),
|
|
98
104
|
});
|
|
99
105
|
export const IssueVettingResultSchema = z.object({
|
|
100
106
|
passedAllChecks: z.boolean(),
|
|
@@ -131,6 +137,7 @@ export const SkippedIssueSchema = z.object({
|
|
|
131
137
|
skippedAt: z.string(),
|
|
132
138
|
});
|
|
133
139
|
// ── Saved candidate schema ─────────────────────────────────────────
|
|
140
|
+
export const HorizonSchema = z.enum(["quick-win", "bigger-bet"]);
|
|
134
141
|
export const SavedCandidateSchema = z.object({
|
|
135
142
|
issueUrl: z.string(),
|
|
136
143
|
repo: z.string(),
|
|
@@ -143,6 +150,7 @@ export const SavedCandidateSchema = z.object({
|
|
|
143
150
|
firstSeenAt: z.string(),
|
|
144
151
|
lastSeenAt: z.string(),
|
|
145
152
|
lastScore: z.number(),
|
|
153
|
+
horizon: HorizonSchema.optional(),
|
|
146
154
|
});
|
|
147
155
|
// ── Scout preferences schema ────────────────────────────────────────
|
|
148
156
|
export const PersistenceModeSchema = z.enum(["local", "gist"]);
|
|
@@ -177,6 +185,18 @@ export const ScoutPreferencesSchema = z.object({
|
|
|
177
185
|
* local network.
|
|
178
186
|
*/
|
|
179
187
|
slmTriageHost: z.string().default(""),
|
|
188
|
+
/**
|
|
189
|
+
* Minimum merged-PR count for a repo to qualify as an anchor in
|
|
190
|
+
* `scout features` (#98). Lowering surfaces more anchors at the cost
|
|
191
|
+
* of weaker prior engagement signal.
|
|
192
|
+
*/
|
|
193
|
+
featuresAnchorThreshold: z.number().int().min(1).max(50).default(3),
|
|
194
|
+
/**
|
|
195
|
+
* Quick-wins / bigger-bets split ratio for `scout features` (#99).
|
|
196
|
+
* 0.6 means 60% quick wins, 40% bigger bets when both pools are
|
|
197
|
+
* abundant. Deficits redirect to the other bucket.
|
|
198
|
+
*/
|
|
199
|
+
featuresSplitRatio: z.number().min(0).max(1).default(0.6),
|
|
180
200
|
});
|
|
181
201
|
// ── Root state schema ───────────────────────────────────────────────
|
|
182
202
|
export const ScoutStateSchema = z.object({
|
|
@@ -61,6 +61,30 @@ export declare function fetchIssuesFromKnownRepos(octokit: Octokit, vetter: Issu
|
|
|
61
61
|
* @param perPage Number of results per API call
|
|
62
62
|
*/
|
|
63
63
|
export declare function searchWithChunkedLabels(octokit: Octokit, labels: string[], reservedOps: number, buildQuery: (labelQuery: string) => string, perPage: number): Promise<GitHubSearchItem[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Build per-call language qualifier strings, fanning out across languages
|
|
66
|
+
* when a multi-language + labels combination would trip GitHub Search's
|
|
67
|
+
* empty-result edge case (multi-`language:` AND with a label OR-group
|
|
68
|
+
* silently returns 0 — see https://github.com/costajohnt/oss-autopilot/issues/1331).
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildLanguageVariants(languages: string[], isAnyLanguage: boolean, hasLabels: boolean): string[];
|
|
71
|
+
/**
|
|
72
|
+
* Search across languages with label chunking, deduplicating results.
|
|
73
|
+
*
|
|
74
|
+
* Fans out one query per language when 2+ languages are paired with labels
|
|
75
|
+
* (works around a GitHub Search backend edge case where the multi-language
|
|
76
|
+
* AND combined with a label OR-group returns 0). For each language variant,
|
|
77
|
+
* delegates to searchWithChunkedLabels to keep within GitHub's 5-operator limit.
|
|
78
|
+
*
|
|
79
|
+
* @param octokit Authenticated Octokit instance
|
|
80
|
+
* @param languages Configured languages (used as `language:X` qualifiers)
|
|
81
|
+
* @param isAnyLanguage When true, skip language qualifiers entirely
|
|
82
|
+
* @param labels Label list passed to searchWithChunkedLabels
|
|
83
|
+
* @param buildBaseQuery Builds the query prefix from a language qualifier string;
|
|
84
|
+
* e.g. `(langQ) => `is:issue is:open ${langQ} no:assignee`.trim()`
|
|
85
|
+
* @param perPage Results per API call
|
|
86
|
+
*/
|
|
87
|
+
export declare function searchAcrossLanguagesAndLabels(octokit: Octokit, languages: string[], isAnyLanguage: boolean, labels: string[], buildBaseQuery: (langQuery: string) => string, perPage: number): Promise<GitHubSearchItem[]>;
|
|
64
88
|
/**
|
|
65
89
|
* Shared pipeline: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
66
90
|
* Used by Phases 2 and 3 to convert raw search results into vetted candidates.
|
|
@@ -291,6 +291,56 @@ export async function searchWithChunkedLabels(octokit, labels, reservedOps, buil
|
|
|
291
291
|
}
|
|
292
292
|
return allItems;
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Build per-call language qualifier strings, fanning out across languages
|
|
296
|
+
* when a multi-language + labels combination would trip GitHub Search's
|
|
297
|
+
* empty-result edge case (multi-`language:` AND with a label OR-group
|
|
298
|
+
* silently returns 0 — see https://github.com/costajohnt/oss-autopilot/issues/1331).
|
|
299
|
+
*/
|
|
300
|
+
export function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
|
|
301
|
+
if (isAnyLanguage || languages.length === 0)
|
|
302
|
+
return [""];
|
|
303
|
+
if (languages.length === 1)
|
|
304
|
+
return [`language:${languages[0]}`];
|
|
305
|
+
if (!hasLabels)
|
|
306
|
+
return [languages.map((l) => `language:${l}`).join(" ")];
|
|
307
|
+
return languages.map((l) => `language:${l}`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Search across languages with label chunking, deduplicating results.
|
|
311
|
+
*
|
|
312
|
+
* Fans out one query per language when 2+ languages are paired with labels
|
|
313
|
+
* (works around a GitHub Search backend edge case where the multi-language
|
|
314
|
+
* AND combined with a label OR-group returns 0). For each language variant,
|
|
315
|
+
* delegates to searchWithChunkedLabels to keep within GitHub's 5-operator limit.
|
|
316
|
+
*
|
|
317
|
+
* @param octokit Authenticated Octokit instance
|
|
318
|
+
* @param languages Configured languages (used as `language:X` qualifiers)
|
|
319
|
+
* @param isAnyLanguage When true, skip language qualifiers entirely
|
|
320
|
+
* @param labels Label list passed to searchWithChunkedLabels
|
|
321
|
+
* @param buildBaseQuery Builds the query prefix from a language qualifier string;
|
|
322
|
+
* e.g. `(langQ) => `is:issue is:open ${langQ} no:assignee`.trim()`
|
|
323
|
+
* @param perPage Results per API call
|
|
324
|
+
*/
|
|
325
|
+
export async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
|
|
326
|
+
const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
|
|
327
|
+
const seenUrls = new Set();
|
|
328
|
+
const allItems = [];
|
|
329
|
+
for (let i = 0; i < langVariants.length; i++) {
|
|
330
|
+
if (i > 0)
|
|
331
|
+
await sleep(INTER_QUERY_DELAY_MS);
|
|
332
|
+
const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`
|
|
333
|
+
.replace(/ +/g, " ")
|
|
334
|
+
.trim(), perPage);
|
|
335
|
+
for (const item of items) {
|
|
336
|
+
if (!seenUrls.has(item.html_url)) {
|
|
337
|
+
seenUrls.add(item.html_url);
|
|
338
|
+
allItems.push(item);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return allItems;
|
|
343
|
+
}
|
|
294
344
|
/**
|
|
295
345
|
* Shared pipeline: spam-filter, repo-exclusion, vetting, and star-count filter.
|
|
296
346
|
* Used by Phases 2 and 3 to convert raw search results into vetted candidates.
|
package/dist/index.d.ts
CHANGED
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { createScout, OssScout } from "./scout.js";
|
|
18
18
|
export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, AntiLLMPolicyResult, AntiLLMPolicySourceFile, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
|
|
19
|
-
export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, LinkedPR, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, StoredOpenPR, SearchStrategy, SkippedIssue, } from "./core/schemas.js";
|
|
20
|
-
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, } from "./core/schemas.js";
|
|
19
|
+
export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, LinkedPR, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, StoredOpenPR, SearchStrategy, SkippedIssue, Horizon, } from "./core/schemas.js";
|
|
20
|
+
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, HorizonSchema, } from "./core/schemas.js";
|
|
21
21
|
export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
|
|
22
22
|
export { IssueDiscovery } from "./core/issue-discovery.js";
|
|
23
|
-
export { IssueVetter, type ScoutStateReader } from "./core/issue-vetting.js";
|
|
23
|
+
export { IssueVetter, type ScoutStateReader, type FeatureSignals, } from "./core/issue-vetting.js";
|
|
24
24
|
export { scanForAntiLLMPolicy, ANTI_LLM_KEYWORDS, } from "./core/anti-llm-policy.js";
|
|
25
|
+
export { discoverFeatures, resolveAnchorRepos, classifyHorizon, splitByHorizon, ANCHOR_THRESHOLD, FEATURE_LABELS, NO_ANCHORS_MESSAGE, NO_RESULTS_MESSAGE, type FeatureCandidate, type FeatureSearchResult, type DiscoverFeaturesOptions, } from "./core/feature-discovery.js";
|
|
26
|
+
export { isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from "./core/linked-pr.js";
|
|
27
|
+
export { fetchRoadmapIssueRefs, parseRoadmapIssueRefs, } from "./core/roadmap.js";
|
package/dist/index.js
CHANGED
|
@@ -17,10 +17,16 @@
|
|
|
17
17
|
// Main API
|
|
18
18
|
export { createScout, OssScout } from "./scout.js";
|
|
19
19
|
// Schemas (for consumers who need runtime validation)
|
|
20
|
-
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, } from "./core/schemas.js";
|
|
20
|
+
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, HorizonSchema, } from "./core/schemas.js";
|
|
21
21
|
// Utilities
|
|
22
22
|
export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
|
|
23
23
|
// Internal classes (for advanced use)
|
|
24
24
|
export { IssueDiscovery } from "./core/issue-discovery.js";
|
|
25
|
-
export { IssueVetter } from "./core/issue-vetting.js";
|
|
25
|
+
export { IssueVetter, } from "./core/issue-vetting.js";
|
|
26
26
|
export { scanForAntiLLMPolicy, ANTI_LLM_KEYWORDS, } from "./core/anti-llm-policy.js";
|
|
27
|
+
// Feature discovery API
|
|
28
|
+
export { discoverFeatures, resolveAnchorRepos, classifyHorizon, splitByHorizon, ANCHOR_THRESHOLD, FEATURE_LABELS, NO_ANCHORS_MESSAGE, NO_RESULTS_MESSAGE, } from "./core/feature-discovery.js";
|
|
29
|
+
// Linked-PR helpers (#97)
|
|
30
|
+
export { isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from "./core/linked-pr.js";
|
|
31
|
+
// Roadmap scraping (#95)
|
|
32
|
+
export { fetchRoadmapIssueRefs, parseRoadmapIssueRefs, } from "./core/roadmap.js";
|
package/dist/scout.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Implements ScoutStateReader to bridge state with the search engine.
|
|
6
6
|
*/
|
|
7
7
|
import type { ScoutStateReader } from "./core/issue-vetting.js";
|
|
8
|
-
import
|
|
8
|
+
import { type FeatureSearchResult } from "./core/feature-discovery.js";
|
|
9
|
+
import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate, SkippedIssue, Horizon } from "./core/schemas.js";
|
|
9
10
|
import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from "./core/types.js";
|
|
10
11
|
import { GistStateStore } from "./core/gist-state-store.js";
|
|
11
12
|
/**
|
|
@@ -51,6 +52,26 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
51
52
|
* Vet a single issue URL for claimability.
|
|
52
53
|
*/
|
|
53
54
|
vetIssue(issueUrl: string): Promise<IssueCandidate>;
|
|
55
|
+
/**
|
|
56
|
+
* `scout features` — surfaces feature-scoped contribution opportunities
|
|
57
|
+
* in repos where the user has 3+ merged PRs (configurable via
|
|
58
|
+
* `featuresAnchorThreshold`), ranked into separate "quick wins" and
|
|
59
|
+
* "bigger bets" buckets (split via `featuresSplitRatio`).
|
|
60
|
+
*
|
|
61
|
+
* Per-call `anchorThreshold` and `splitRatio` overrides take precedence
|
|
62
|
+
* over the persisted preferences.
|
|
63
|
+
*
|
|
64
|
+
* When `broad` is true (#100), bypasses anchor resolution and runs a
|
|
65
|
+
* cross-repo GitHub Search query for first-touch contributors who
|
|
66
|
+
* haven't yet built repo relationships. Filters by user language
|
|
67
|
+
* preferences and excluded repos/orgs.
|
|
68
|
+
*/
|
|
69
|
+
features(options?: {
|
|
70
|
+
count?: number;
|
|
71
|
+
anchorThreshold?: number;
|
|
72
|
+
splitRatio?: number;
|
|
73
|
+
broad?: boolean;
|
|
74
|
+
}): Promise<FeatureSearchResult>;
|
|
54
75
|
/**
|
|
55
76
|
* Re-vet all saved results with bounded concurrency.
|
|
56
77
|
* Classifies each as still_available, claimed, has_pr, closed, or error.
|
|
@@ -106,7 +127,9 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
106
127
|
* If a candidate already exists, updates score/recommendation/lastSeenAt
|
|
107
128
|
* but preserves firstSeenAt.
|
|
108
129
|
*/
|
|
109
|
-
saveResults(candidates: IssueCandidate
|
|
130
|
+
saveResults(candidates: Array<IssueCandidate | (IssueCandidate & {
|
|
131
|
+
horizon?: Horizon;
|
|
132
|
+
})>): void;
|
|
110
133
|
/**
|
|
111
134
|
* Get all saved results.
|
|
112
135
|
*/
|
package/dist/scout.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Implements ScoutStateReader to bridge state with the search engine.
|
|
6
6
|
*/
|
|
7
7
|
import { IssueDiscovery } from "./core/issue-discovery.js";
|
|
8
|
+
import { IssueVetter } from "./core/issue-vetting.js";
|
|
9
|
+
import { discoverFeatures, discoverFeaturesBroad, } from "./core/feature-discovery.js";
|
|
8
10
|
import { ScoutStateSchema } from "./core/schemas.js";
|
|
9
11
|
import { GistStateStore, mergeStates } from "./core/gist-state-store.js";
|
|
10
12
|
import { getOctokit } from "./core/github.js";
|
|
@@ -164,6 +166,48 @@ export class OssScout {
|
|
|
164
166
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
165
167
|
return discovery.vetIssue(issueUrl);
|
|
166
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* `scout features` — surfaces feature-scoped contribution opportunities
|
|
171
|
+
* in repos where the user has 3+ merged PRs (configurable via
|
|
172
|
+
* `featuresAnchorThreshold`), ranked into separate "quick wins" and
|
|
173
|
+
* "bigger bets" buckets (split via `featuresSplitRatio`).
|
|
174
|
+
*
|
|
175
|
+
* Per-call `anchorThreshold` and `splitRatio` overrides take precedence
|
|
176
|
+
* over the persisted preferences.
|
|
177
|
+
*
|
|
178
|
+
* When `broad` is true (#100), bypasses anchor resolution and runs a
|
|
179
|
+
* cross-repo GitHub Search query for first-touch contributors who
|
|
180
|
+
* haven't yet built repo relationships. Filters by user language
|
|
181
|
+
* preferences and excluded repos/orgs.
|
|
182
|
+
*/
|
|
183
|
+
async features(options) {
|
|
184
|
+
const count = options?.count ?? 10;
|
|
185
|
+
const octokit = getOctokit(this.githubToken);
|
|
186
|
+
const vetter = new IssueVetter(octokit, this);
|
|
187
|
+
const result = options?.broad
|
|
188
|
+
? await discoverFeaturesBroad({
|
|
189
|
+
octokit,
|
|
190
|
+
vetter,
|
|
191
|
+
count,
|
|
192
|
+
languages: this.state.preferences.languages,
|
|
193
|
+
excludeRepos: this.state.preferences.excludeRepos,
|
|
194
|
+
excludeOrgs: this.state.preferences.excludeOrgs,
|
|
195
|
+
splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio,
|
|
196
|
+
})
|
|
197
|
+
: await discoverFeatures({
|
|
198
|
+
octokit,
|
|
199
|
+
vetter,
|
|
200
|
+
repoScores: this.state.repoScores ?? {},
|
|
201
|
+
count,
|
|
202
|
+
anchorThreshold: options?.anchorThreshold ??
|
|
203
|
+
this.state.preferences.featuresAnchorThreshold,
|
|
204
|
+
splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio,
|
|
205
|
+
});
|
|
206
|
+
this.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
207
|
+
this.state.lastSearchAt = new Date().toISOString();
|
|
208
|
+
this.dirty = true;
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
167
211
|
// ── Batch Vetting ───────────────────────────────────────────────────
|
|
168
212
|
/**
|
|
169
213
|
* Re-vet all saved results with bounded concurrency.
|
|
@@ -423,6 +467,7 @@ export class OssScout {
|
|
|
423
467
|
firstSeenAt: prev?.firstSeenAt ?? now,
|
|
424
468
|
lastSeenAt: now,
|
|
425
469
|
lastScore: c.viabilityScore,
|
|
470
|
+
horizon: "horizon" in c ? c.horizon : undefined,
|
|
426
471
|
});
|
|
427
472
|
}
|
|
428
473
|
this.state.savedResults = [...existing.values()];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Personalized GitHub issue finder with multi-strategy search, deep vetting, and viability scoring — CLI, library, MCP server, and Claude Code plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|