@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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - repo-health.ts — project health, contribution guidelines
|
|
8
8
|
*/
|
|
9
9
|
import { parseGitHubUrl } from "./utils.js";
|
|
10
|
-
import { ValidationError, errorMessage, isRateLimitError } from "./errors.js";
|
|
10
|
+
import { ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, } from "./errors.js";
|
|
11
11
|
import { debug, warn } from "./logger.js";
|
|
12
12
|
import { calculateRepoQualityBonus, calculateViabilityScore, } from "./issue-scoring.js";
|
|
13
13
|
import { repoBelongsToCategory } from "./category-mapping.js";
|
|
@@ -31,11 +31,19 @@ export class IssueVetter {
|
|
|
31
31
|
/**
|
|
32
32
|
* Vet a specific issue — runs all checks and computes recommendation + viability score.
|
|
33
33
|
* Results are cached for 15 minutes to avoid redundant API calls on repeated searches.
|
|
34
|
+
*
|
|
35
|
+
* `opts.featureSignals` are forwarded directly to scoring; the vetter does
|
|
36
|
+
* not derive them from the fetched issue. Cache key includes a digest of
|
|
37
|
+
* the signals so the same URL with different signals doesn't return a
|
|
38
|
+
* stale score.
|
|
34
39
|
*/
|
|
35
|
-
async vetIssue(issueUrl) {
|
|
40
|
+
async vetIssue(issueUrl, opts) {
|
|
36
41
|
// Check vetting cache first — avoids ~6+ API calls per issue
|
|
37
42
|
const cache = getHttpCache();
|
|
38
|
-
const
|
|
43
|
+
const sigKey = opts?.featureSignals
|
|
44
|
+
? `: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}`
|
|
45
|
+
: "";
|
|
46
|
+
const cacheKey = `vet:${issueUrl}${sigKey}`;
|
|
39
47
|
const cached = cache.getIfFresh(cacheKey, VETTING_CACHE_TTL_MS);
|
|
40
48
|
if (cached &&
|
|
41
49
|
typeof cached === "object" &&
|
|
@@ -213,6 +221,7 @@ export class IssueVetter {
|
|
|
213
221
|
orgHasMergedPRs,
|
|
214
222
|
repoQualityBonus,
|
|
215
223
|
matchesPreferredCategory: matchesCategory,
|
|
224
|
+
featureSignals: opts?.featureSignals,
|
|
216
225
|
});
|
|
217
226
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
218
227
|
let searchPriority = "normal";
|
|
@@ -263,9 +272,16 @@ export class IssueVetter {
|
|
|
263
272
|
let failedVettingCount = 0;
|
|
264
273
|
let rateLimitFailures = 0;
|
|
265
274
|
let attemptedCount = 0;
|
|
275
|
+
// Capture the first 401 so we can re-throw after in-flight tasks settle.
|
|
276
|
+
// Per-item tolerance is right for transient failures, but a 401 means
|
|
277
|
+
// the token is invalid and no other issue will succeed either —
|
|
278
|
+
// continuing to log per-issue warnings buries the actual problem.
|
|
279
|
+
let firstAuthError = null;
|
|
266
280
|
for (const url of urls) {
|
|
267
281
|
if (candidates.length >= maxResults)
|
|
268
282
|
break;
|
|
283
|
+
if (firstAuthError)
|
|
284
|
+
break; // stop scheduling once auth has failed
|
|
269
285
|
attemptedCount++;
|
|
270
286
|
const task = this.vetIssue(url)
|
|
271
287
|
.then((candidate) => {
|
|
@@ -278,6 +294,10 @@ export class IssueVetter {
|
|
|
278
294
|
}
|
|
279
295
|
})
|
|
280
296
|
.catch((error) => {
|
|
297
|
+
if (getHttpStatusCode(error) === 401) {
|
|
298
|
+
firstAuthError ??= error;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
281
301
|
failedVettingCount++;
|
|
282
302
|
if (isRateLimitError(error)) {
|
|
283
303
|
rateLimitFailures++;
|
|
@@ -293,6 +313,12 @@ export class IssueVetter {
|
|
|
293
313
|
}
|
|
294
314
|
// Wait for remaining
|
|
295
315
|
await Promise.allSettled(pending.values());
|
|
316
|
+
if (firstAuthError) {
|
|
317
|
+
if (candidates.length > 0) {
|
|
318
|
+
warn(MODULE, `Auth failed mid-batch after ${candidates.length} successful vet(s) — discarding partial results`);
|
|
319
|
+
}
|
|
320
|
+
throw firstAuthError;
|
|
321
|
+
}
|
|
296
322
|
const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
|
|
297
323
|
if (allFailed) {
|
|
298
324
|
warn(MODULE, `All ${attemptedCount} issue(s) failed vetting. ` +
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for reasoning about linked PRs surfaced via the issue timeline.
|
|
3
|
+
*
|
|
4
|
+
* Currently scoped to detecting "stalled" PRs — open PRs that have not been
|
|
5
|
+
* updated in the last `STALLED_PR_THRESHOLD_DAYS` days. Stalled linked PRs
|
|
6
|
+
* are surfaced as revive opportunities (#97). Note: this module does NOT
|
|
7
|
+
* change scoring; the existing -30 penalty on issues with linked PRs is
|
|
8
|
+
* preserved. This is annotation-only.
|
|
9
|
+
*/
|
|
10
|
+
import type { LinkedPR } from "./schemas.js";
|
|
11
|
+
/** Days of inactivity that classify a linked PR as "stalled". */
|
|
12
|
+
export declare const STALLED_PR_THRESHOLD_DAYS = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Determine whether a linked PR is stalled (open + not updated in the
|
|
15
|
+
* last STALLED_PR_THRESHOLD_DAYS days). Returns false when the PR is
|
|
16
|
+
* closed/merged, when updatedAt is missing, or when the threshold isn't met.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isLinkedPRStalled(linkedPR: LinkedPR | null | undefined, now?: Date, thresholdDays?: number): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for reasoning about linked PRs surfaced via the issue timeline.
|
|
3
|
+
*
|
|
4
|
+
* Currently scoped to detecting "stalled" PRs — open PRs that have not been
|
|
5
|
+
* updated in the last `STALLED_PR_THRESHOLD_DAYS` days. Stalled linked PRs
|
|
6
|
+
* are surfaced as revive opportunities (#97). Note: this module does NOT
|
|
7
|
+
* change scoring; the existing -30 penalty on issues with linked PRs is
|
|
8
|
+
* preserved. This is annotation-only.
|
|
9
|
+
*/
|
|
10
|
+
/** Days of inactivity that classify a linked PR as "stalled". */
|
|
11
|
+
export const STALLED_PR_THRESHOLD_DAYS = 30;
|
|
12
|
+
/**
|
|
13
|
+
* Determine whether a linked PR is stalled (open + not updated in the
|
|
14
|
+
* last STALLED_PR_THRESHOLD_DAYS days). Returns false when the PR is
|
|
15
|
+
* closed/merged, when updatedAt is missing, or when the threshold isn't met.
|
|
16
|
+
*/
|
|
17
|
+
export function isLinkedPRStalled(linkedPR, now = new Date(), thresholdDays = STALLED_PR_THRESHOLD_DAYS) {
|
|
18
|
+
if (!linkedPR || linkedPR.state !== "open" || !linkedPR.updatedAt)
|
|
19
|
+
return false;
|
|
20
|
+
const updated = new Date(linkedPR.updatedAt);
|
|
21
|
+
if (Number.isNaN(updated.getTime()))
|
|
22
|
+
return false;
|
|
23
|
+
const ageDays = (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24);
|
|
24
|
+
return ageDays >= thresholdDays;
|
|
25
|
+
}
|
package/dist/core/repo-health.js
CHANGED
|
@@ -73,6 +73,9 @@ export async function checkProjectHealth(octokit, owner, repo) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
catch (error) {
|
|
76
|
+
if (getHttpStatusCode(error) === 401 || isRateLimitError(error)) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
76
79
|
const errMsg = errorMessage(error);
|
|
77
80
|
warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
78
81
|
return {
|
|
@@ -0,0 +1,38 @@
|
|
|
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 type { Octokit } from "@octokit/rest";
|
|
14
|
+
/**
|
|
15
|
+
* Parse markdown content for issue number references.
|
|
16
|
+
*
|
|
17
|
+
* Extracts:
|
|
18
|
+
* - bare `#NNN` references that are not on a markdown heading line
|
|
19
|
+
* - `<owner>/<repo>#NNN` cross-repo refs that match the repo we're parsing for
|
|
20
|
+
* - `https://github.com/<owner>/<repo>/issues/NNN` URLs that match the
|
|
21
|
+
* repo we're parsing for (URLs to other repos are ignored)
|
|
22
|
+
*
|
|
23
|
+
* The match for cross-repo `owner/repo#N` and full URLs is intentional:
|
|
24
|
+
* scattered references to unrelated repos in a roadmap shouldn't bump
|
|
25
|
+
* scores in those repos.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseRoadmapIssueRefs(content: string, owner: string, repo: string): Set<number>;
|
|
28
|
+
/**
|
|
29
|
+
* Fetch and parse the repo's ROADMAP.md, returning the set of referenced
|
|
30
|
+
* issue numbers. Returns an empty set if no roadmap is found.
|
|
31
|
+
*
|
|
32
|
+
* Probes ROADMAP_PATHS sequentially until a 200 response is received.
|
|
33
|
+
* Auth/rate-limit errors propagate; other errors are logged and degrade
|
|
34
|
+
* to an empty set.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fetchRoadmapIssueRefs(octokit: Octokit, owner: string, repo: string): Promise<Set<number>>;
|
|
37
|
+
/** Test-only: clear the in-memory cache. */
|
|
38
|
+
export declare function _clearRoadmapCacheForTests(): void;
|
|
@@ -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({
|
|
@@ -370,6 +370,8 @@ export async function searchInRepos(octokit, vetter, repos, baseQualifiers, labe
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
catch (error) {
|
|
373
|
+
if (getHttpStatusCode(error) === 401)
|
|
374
|
+
throw error;
|
|
373
375
|
failedBatches++;
|
|
374
376
|
if (isRateLimitError(error)) {
|
|
375
377
|
rateLimitFailures++;
|
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
|
*/
|