@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/scout.js
CHANGED
|
@@ -5,12 +5,30 @@
|
|
|
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";
|
|
11
13
|
import { loadLocalState } from "./core/local-state.js";
|
|
12
14
|
import { warn } from "./core/logger.js";
|
|
13
15
|
import { extractRepoFromUrl } from "./core/utils.js";
|
|
16
|
+
import { errorMessage, getHttpStatusCode, isRateLimitError, } from "./core/errors.js";
|
|
17
|
+
/** Cause-specific user-facing message for degraded (offline) mode. */
|
|
18
|
+
function offlineModeMessage(reason) {
|
|
19
|
+
const tail = "Changes will only be saved locally.";
|
|
20
|
+
switch (reason) {
|
|
21
|
+
case "rate_limit":
|
|
22
|
+
return `Gist sync unavailable — GitHub API rate limit exceeded. ${tail} Try again after the rate limit resets.`;
|
|
23
|
+
case "network":
|
|
24
|
+
return `Gist sync unavailable — could not reach GitHub. ${tail} Check your network connection.`;
|
|
25
|
+
case "server":
|
|
26
|
+
return `Gist sync unavailable — GitHub returned a server error. ${tail} Try again later.`;
|
|
27
|
+
case "unknown":
|
|
28
|
+
case undefined:
|
|
29
|
+
return `Gist sync unavailable — running in offline mode. ${tail}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
14
32
|
/** Wrap a real Octokit instance as GistOctokitLike without unsafe double casts. */
|
|
15
33
|
function toGistOctokit(octokit) {
|
|
16
34
|
return {
|
|
@@ -84,7 +102,7 @@ export async function createScout(config) {
|
|
|
84
102
|
gistStore = new GistStateStore(toGistOctokit(getOctokit(config.githubToken)));
|
|
85
103
|
const result = await gistStore.bootstrap();
|
|
86
104
|
if (result.degraded) {
|
|
87
|
-
warn("scout",
|
|
105
|
+
warn("scout", offlineModeMessage(result.degradedReason));
|
|
88
106
|
}
|
|
89
107
|
const localState = loadLocalState();
|
|
90
108
|
state = mergeStates(localState, result.state);
|
|
@@ -148,6 +166,48 @@ export class OssScout {
|
|
|
148
166
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
149
167
|
return discovery.vetIssue(issueUrl);
|
|
150
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
|
+
}
|
|
151
211
|
// ── Batch Vetting ───────────────────────────────────────────────────
|
|
152
212
|
/**
|
|
153
213
|
* Re-vet all saved results with bounded concurrency.
|
|
@@ -159,7 +219,15 @@ export class OssScout {
|
|
|
159
219
|
const concurrency = options?.concurrency ?? 5;
|
|
160
220
|
const results = [];
|
|
161
221
|
const pending = new Map();
|
|
222
|
+
// First 401 OR rate-limit short-circuits the whole batch. Unlike
|
|
223
|
+
// vetIssuesParallel (which has a batch-level rateLimitHit flag the
|
|
224
|
+
// search orchestrator surfaces via rateLimitWarning), vetList is the
|
|
225
|
+
// user-facing CLI entry point — N rows of "rate limit exceeded" is the
|
|
226
|
+
// exact silent-failure mode the documented strategy aims to prevent.
|
|
227
|
+
let firstHardError = null;
|
|
162
228
|
for (const item of saved) {
|
|
229
|
+
if (firstHardError)
|
|
230
|
+
break;
|
|
163
231
|
const task = this.vetIssue(item.issueUrl)
|
|
164
232
|
.then((candidate) => {
|
|
165
233
|
results.push({
|
|
@@ -173,15 +241,19 @@ export class OssScout {
|
|
|
173
241
|
});
|
|
174
242
|
})
|
|
175
243
|
.catch((error) => {
|
|
176
|
-
|
|
177
|
-
|
|
244
|
+
if (getHttpStatusCode(error) === 401 || isRateLimitError(error)) {
|
|
245
|
+
firstHardError ??= error;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const status = getHttpStatusCode(error);
|
|
249
|
+
const isGone = status === 404 || status === 410;
|
|
178
250
|
results.push({
|
|
179
251
|
issueUrl: item.issueUrl,
|
|
180
252
|
repo: item.repo,
|
|
181
253
|
number: item.number,
|
|
182
254
|
title: item.title,
|
|
183
255
|
status: isGone ? "closed" : "error",
|
|
184
|
-
errorMessage:
|
|
256
|
+
errorMessage: errorMessage(error),
|
|
185
257
|
});
|
|
186
258
|
})
|
|
187
259
|
.finally(() => {
|
|
@@ -193,6 +265,12 @@ export class OssScout {
|
|
|
193
265
|
}
|
|
194
266
|
}
|
|
195
267
|
await Promise.allSettled(pending.values());
|
|
268
|
+
if (firstHardError) {
|
|
269
|
+
if (results.length > 0) {
|
|
270
|
+
warn("scout", `vetList aborted mid-batch after ${results.length} result(s) — discarding partial results due to auth/rate-limit failure`);
|
|
271
|
+
}
|
|
272
|
+
throw firstHardError;
|
|
273
|
+
}
|
|
196
274
|
const summary = {
|
|
197
275
|
total: results.length,
|
|
198
276
|
stillAvailable: results.filter((r) => r.status === "still_available")
|
|
@@ -389,6 +467,7 @@ export class OssScout {
|
|
|
389
467
|
firstSeenAt: prev?.firstSeenAt ?? now,
|
|
390
468
|
lastSeenAt: now,
|
|
391
469
|
lastScore: c.viabilityScore,
|
|
470
|
+
horizon: "horizon" in c ? c.horizon : undefined,
|
|
392
471
|
});
|
|
393
472
|
}
|
|
394
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.0",
|
|
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": {
|