@oss-scout/core 0.11.0 → 1.1.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 +89 -66
- package/dist/cli.js +302 -436
- package/dist/commands/command-scout.d.ts +21 -0
- package/dist/commands/command-scout.js +21 -0
- package/dist/commands/config.js +10 -128
- package/dist/commands/features.js +15 -28
- package/dist/commands/results.d.ts +13 -2
- package/dist/commands/results.js +29 -2
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.js +65 -70
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +35 -6
- package/dist/commands/skip.d.ts +4 -0
- package/dist/commands/skip.js +45 -55
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.js +10 -0
- package/dist/commands/vet-list.js +3 -19
- package/dist/commands/vet.js +18 -25
- package/dist/commands/with-scout.d.ts +32 -0
- package/dist/commands/with-scout.js +41 -0
- package/dist/core/anti-llm-policy.js +5 -33
- package/dist/core/bootstrap.d.ts +2 -2
- package/dist/core/bootstrap.js +5 -9
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.js +20 -5
- package/dist/core/feature-discovery.d.ts +13 -1
- package/dist/core/feature-discovery.js +104 -81
- package/dist/core/gist-state-store.d.ts +13 -12
- package/dist/core/gist-state-store.js +128 -53
- package/dist/core/http-cache.d.ts +32 -2
- package/dist/core/http-cache.js +74 -19
- package/dist/core/issue-discovery.d.ts +12 -1
- package/dist/core/issue-discovery.js +94 -67
- package/dist/core/issue-eligibility.d.ts +11 -4
- package/dist/core/issue-eligibility.js +124 -69
- package/dist/core/issue-graphql.d.ts +58 -0
- package/dist/core/issue-graphql.js +108 -0
- package/dist/core/issue-vetting.d.ts +115 -9
- package/dist/core/issue-vetting.js +246 -109
- package/dist/core/local-state.d.ts +6 -2
- package/dist/core/local-state.js +23 -5
- package/dist/core/logger.d.ts +12 -4
- package/dist/core/logger.js +33 -7
- package/dist/core/personalization.d.ts +30 -10
- package/dist/core/personalization.js +64 -24
- package/dist/core/preference-fields.d.ts +47 -0
- package/dist/core/preference-fields.js +180 -0
- package/dist/core/probe-repo-file.d.ts +47 -0
- package/dist/core/probe-repo-file.js +57 -0
- package/dist/core/repo-health.js +40 -32
- package/dist/core/roadmap.js +26 -22
- package/dist/core/schemas.d.ts +148 -26
- package/dist/core/schemas.js +83 -17
- package/dist/core/search-budget.d.ts +9 -0
- package/dist/core/search-budget.js +36 -3
- package/dist/core/search-phases.d.ts +4 -21
- package/dist/core/search-phases.js +37 -89
- package/dist/core/types.d.ts +151 -38
- package/dist/core/utils.js +60 -26
- package/dist/formatters/human.d.ts +60 -0
- package/dist/formatters/human.js +199 -0
- package/dist/formatters/markdown.d.ts +10 -0
- package/dist/formatters/markdown.js +31 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +8 -0
- package/dist/scout.d.ts +75 -12
- package/dist/scout.js +265 -26
- package/package.json +1 -1
package/dist/commands/skip.js
CHANGED
|
@@ -1,52 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skip command — manage the skip list for excluding issues from future searches.
|
|
3
3
|
*/
|
|
4
|
-
import { loadLocalState
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Uses gist persistence when a token is available, otherwise provided-state mode.
|
|
10
|
-
*/
|
|
11
|
-
async function createSkipScout(state) {
|
|
12
|
-
const token = getGitHubToken() ?? "";
|
|
13
|
-
if (token) {
|
|
14
|
-
return createScout({
|
|
15
|
-
githubToken: token,
|
|
16
|
-
persistence: "provided",
|
|
17
|
-
initialState: state,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
return createScout({
|
|
21
|
-
githubToken: "",
|
|
22
|
-
persistence: "provided",
|
|
23
|
-
initialState: state,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
4
|
+
import { loadLocalState } from "../core/local-state.js";
|
|
5
|
+
import { withScout, persistScout } from "./with-scout.js";
|
|
6
|
+
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, } from "./validation.js";
|
|
7
|
+
// Skip operations are local-only, so they don't require a GitHub token.
|
|
8
|
+
const SKIP_SCOUT_OPTIONS = { requireToken: false };
|
|
26
9
|
/**
|
|
27
10
|
* Skip an issue by URL — adds it to the skip list and removes it from saved results.
|
|
28
11
|
* Tries to enrich metadata from saved results if available.
|
|
29
12
|
*/
|
|
30
13
|
export async function runSkip(options) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
14
|
+
// Validate up front: skip matching is exact-URL, so a junk or near-miss
|
|
15
|
+
// URL (trailing slash, query string) would be stored but never exclude
|
|
16
|
+
// anything — a silent no-op. Reject it with the expected format instead.
|
|
17
|
+
validateUrl(options.issueUrl);
|
|
18
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
19
|
+
return withScout(options.state, async (scout) => {
|
|
20
|
+
const alreadySkipped = scout
|
|
21
|
+
.getSkippedIssues()
|
|
22
|
+
.some((s) => s.url === options.issueUrl);
|
|
23
|
+
if (alreadySkipped) {
|
|
24
|
+
return { skipped: false, alreadySkipped: true };
|
|
25
|
+
}
|
|
26
|
+
// Try to enrich metadata from saved results
|
|
27
|
+
const saved = scout
|
|
28
|
+
.getSavedResults()
|
|
29
|
+
.find((r) => r.issueUrl === options.issueUrl);
|
|
30
|
+
const metadata = saved
|
|
31
|
+
? { repo: saved.repo, number: saved.number, title: saved.title }
|
|
32
|
+
: parseIssueUrl(options.issueUrl);
|
|
33
|
+
scout.skipIssue(options.issueUrl, metadata);
|
|
34
|
+
// Persist only on an actual change so an already-skipped no-op doesn't
|
|
35
|
+
// trigger a needless gist push.
|
|
36
|
+
await persistScout(scout);
|
|
37
|
+
return { skipped: true, alreadySkipped: false };
|
|
38
|
+
}, SKIP_SCOUT_OPTIONS);
|
|
50
39
|
}
|
|
51
40
|
/**
|
|
52
41
|
* List all skipped issues.
|
|
@@ -59,30 +48,31 @@ export function runSkipList(options) {
|
|
|
59
48
|
* Clear all skipped issues.
|
|
60
49
|
*/
|
|
61
50
|
export async function runSkipClear() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
saveLocalState(scout.getState());
|
|
66
|
-
await scout.checkpoint();
|
|
51
|
+
await withScout(undefined, (scout) => {
|
|
52
|
+
scout.clearSkippedIssues();
|
|
53
|
+
}, { ...SKIP_SCOUT_OPTIONS, persist: true });
|
|
67
54
|
}
|
|
68
55
|
/**
|
|
69
56
|
* Remove a specific issue from the skip list (unskip).
|
|
57
|
+
*
|
|
58
|
+
* Deliberately does NOT validate the URL: entries stored before skip-add
|
|
59
|
+
* validation existed may be junk, and exact-match removal is the only way
|
|
60
|
+
* to clean them up short of `skip clear`.
|
|
70
61
|
*/
|
|
71
62
|
export async function runSkipRemove(options) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return { removed };
|
|
63
|
+
return withScout(undefined, async (scout) => {
|
|
64
|
+
const before = scout.getSkippedIssues().length;
|
|
65
|
+
scout.unskipIssue(options.issueUrl);
|
|
66
|
+
const removed = before !== scout.getSkippedIssues().length;
|
|
67
|
+
await persistScout(scout);
|
|
68
|
+
return { removed };
|
|
69
|
+
}, SKIP_SCOUT_OPTIONS);
|
|
80
70
|
}
|
|
81
71
|
/**
|
|
82
72
|
* Parse a GitHub issue URL to extract repo and number.
|
|
83
73
|
*/
|
|
84
74
|
function parseIssueUrl(url) {
|
|
85
|
-
const match = url.match(
|
|
75
|
+
const match = url.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/);
|
|
86
76
|
if (!match)
|
|
87
77
|
return undefined;
|
|
88
78
|
return { repo: match[1], number: parseInt(match[2], 10), title: "" };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync command — reconcile tracked open PRs against their current GitHub state
|
|
3
|
+
* (#164). Records merges/closures, prunes resolved entries, and recomputes repo
|
|
4
|
+
* scores. Cheaper than a full bootstrap; meant for periodic / daily runs.
|
|
5
|
+
*/
|
|
6
|
+
import type { ScoutState } from "../core/schemas.js";
|
|
7
|
+
import type { SyncResult } from "../core/types.js";
|
|
8
|
+
export declare function runSync(options?: {
|
|
9
|
+
state?: ScoutState;
|
|
10
|
+
}): Promise<SyncResult>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync command — reconcile tracked open PRs against their current GitHub state
|
|
3
|
+
* (#164). Records merges/closures, prunes resolved entries, and recomputes repo
|
|
4
|
+
* scores. Cheaper than a full bootstrap; meant for periodic / daily runs.
|
|
5
|
+
*/
|
|
6
|
+
import { withScout } from "./with-scout.js";
|
|
7
|
+
export async function runSync(options) {
|
|
8
|
+
// syncOpenPRs checkpoints itself, so withScout doesn't need to persist.
|
|
9
|
+
return withScout(options?.state, (scout) => scout.syncOpenPRs());
|
|
10
|
+
}
|
|
@@ -1,23 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { requireGitHubToken } from "../core/utils.js";
|
|
3
|
-
import { saveLocalState } from "../core/local-state.js";
|
|
1
|
+
import { withScout } from "./with-scout.js";
|
|
4
2
|
export async function runVetList(options) {
|
|
5
|
-
|
|
6
|
-
const scout = options.state
|
|
7
|
-
? await createScout({
|
|
8
|
-
githubToken: token,
|
|
9
|
-
persistence: "provided",
|
|
10
|
-
initialState: options.state,
|
|
11
|
-
})
|
|
12
|
-
: await createScout({ githubToken: token });
|
|
13
|
-
const result = await scout.vetList({
|
|
3
|
+
return withScout(options.state, (scout) => scout.vetList({
|
|
14
4
|
concurrency: options.concurrency,
|
|
15
5
|
prune: options.prune,
|
|
16
|
-
});
|
|
17
|
-
saveLocalState(scout.getState());
|
|
18
|
-
const persisted = await scout.checkpoint();
|
|
19
|
-
if (!persisted) {
|
|
20
|
-
console.error("Warning: changes saved locally but gist sync failed.");
|
|
21
|
-
}
|
|
22
|
-
return result;
|
|
6
|
+
}), { persist: true });
|
|
23
7
|
}
|
package/dist/commands/vet.js
CHANGED
|
@@ -1,33 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vet command — vets a specific issue for claimability.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { requireGitHubToken } from "../core/utils.js";
|
|
4
|
+
import { withScout } from "./with-scout.js";
|
|
6
5
|
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, } from "./validation.js";
|
|
7
6
|
export async function runVet(options) {
|
|
8
7
|
validateUrl(options.issueUrl);
|
|
9
8
|
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
recommendation: candidate.recommendation,
|
|
28
|
-
reasonsToApprove: candidate.reasonsToApprove,
|
|
29
|
-
reasonsToSkip: candidate.reasonsToSkip,
|
|
30
|
-
projectHealth: candidate.projectHealth,
|
|
31
|
-
vettingResult: candidate.vettingResult,
|
|
32
|
-
};
|
|
9
|
+
return withScout(options.state, async (scout) => {
|
|
10
|
+
const candidate = await scout.vetIssue(options.issueUrl);
|
|
11
|
+
return {
|
|
12
|
+
issue: {
|
|
13
|
+
repo: candidate.issue.repo,
|
|
14
|
+
number: candidate.issue.number,
|
|
15
|
+
title: candidate.issue.title,
|
|
16
|
+
url: candidate.issue.url,
|
|
17
|
+
labels: candidate.issue.labels,
|
|
18
|
+
},
|
|
19
|
+
recommendation: candidate.recommendation,
|
|
20
|
+
reasonsToApprove: candidate.reasonsToApprove,
|
|
21
|
+
reasonsToSkip: candidate.reasonsToSkip,
|
|
22
|
+
projectHealth: candidate.projectHealth,
|
|
23
|
+
vettingResult: candidate.vettingResult,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
33
26
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared command scaffolding (#154).
|
|
3
|
+
*
|
|
4
|
+
* Every command repeated the same prologue (require token, load state, build
|
|
5
|
+
* the scout) and most repeated the same epilogue (write local state, gist
|
|
6
|
+
* checkpoint, warn on gist failure). The warning text was byte-identical in
|
|
7
|
+
* three files and inconsistently omitted in skip.ts. `withScout` owns the
|
|
8
|
+
* prologue plus an optional persist epilogue; `persistScout` is the epilogue
|
|
9
|
+
* on its own for commands that persist only on some code paths.
|
|
10
|
+
*/
|
|
11
|
+
import type { OssScout } from "../scout.js";
|
|
12
|
+
import type { ScoutState } from "../core/schemas.js";
|
|
13
|
+
/**
|
|
14
|
+
* Write the scout's state to disk and push the gist checkpoint, warning to
|
|
15
|
+
* stderr if the gist push failed (local save still succeeded).
|
|
16
|
+
*/
|
|
17
|
+
export declare function persistScout(scout: OssScout): Promise<void>;
|
|
18
|
+
export interface WithScoutOptions {
|
|
19
|
+
/** Persist via persistScout after fn resolves. Default false. */
|
|
20
|
+
persist?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Require a GitHub token (throws if absent). Default true. Skip-list
|
|
23
|
+
* operations are local-only and pass false so they work without auth.
|
|
24
|
+
*/
|
|
25
|
+
requireToken?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a scout for the given (or loaded) state, run fn against it, and
|
|
29
|
+
* optionally persist afterward. Centralizes the create/persist/warn boilerplate
|
|
30
|
+
* shared by the search, vet, vet-list, features, and skip commands.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withScout<T>(state: ScoutState | undefined, fn: (scout: OssScout) => Promise<T> | T, options?: WithScoutOptions): Promise<T>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared command scaffolding (#154).
|
|
3
|
+
*
|
|
4
|
+
* Every command repeated the same prologue (require token, load state, build
|
|
5
|
+
* the scout) and most repeated the same epilogue (write local state, gist
|
|
6
|
+
* checkpoint, warn on gist failure). The warning text was byte-identical in
|
|
7
|
+
* three files and inconsistently omitted in skip.ts. `withScout` owns the
|
|
8
|
+
* prologue plus an optional persist epilogue; `persistScout` is the epilogue
|
|
9
|
+
* on its own for commands that persist only on some code paths.
|
|
10
|
+
*/
|
|
11
|
+
import { buildCommandScout } from "./command-scout.js";
|
|
12
|
+
import { requireGitHubToken, getGitHubToken } from "../core/utils.js";
|
|
13
|
+
import { loadLocalState, saveLocalState } from "../core/local-state.js";
|
|
14
|
+
const GIST_SYNC_WARNING = "Warning: changes saved locally but gist sync failed.";
|
|
15
|
+
/**
|
|
16
|
+
* Write the scout's state to disk and push the gist checkpoint, warning to
|
|
17
|
+
* stderr if the gist push failed (local save still succeeded).
|
|
18
|
+
*/
|
|
19
|
+
export async function persistScout(scout) {
|
|
20
|
+
saveLocalState(scout.getState());
|
|
21
|
+
const persisted = await scout.checkpoint();
|
|
22
|
+
if (!persisted) {
|
|
23
|
+
console.error(GIST_SYNC_WARNING);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a scout for the given (or loaded) state, run fn against it, and
|
|
28
|
+
* optionally persist afterward. Centralizes the create/persist/warn boilerplate
|
|
29
|
+
* shared by the search, vet, vet-list, features, and skip commands.
|
|
30
|
+
*/
|
|
31
|
+
export async function withScout(state, fn, options = {}) {
|
|
32
|
+
const { persist = false, requireToken = true } = options;
|
|
33
|
+
const token = requireToken ? requireGitHubToken() : (getGitHubToken() ?? "");
|
|
34
|
+
const resolvedState = state ?? loadLocalState();
|
|
35
|
+
const scout = await buildCommandScout(resolvedState, token);
|
|
36
|
+
const result = await fn(scout);
|
|
37
|
+
if (persist) {
|
|
38
|
+
await persistScout(scout);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* can rely on a structured `AntiLLMPolicyResult` rather than re-implementing
|
|
8
8
|
* the scan in agent prose.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
const MODULE = "anti-llm-policy";
|
|
10
|
+
import { getHttpStatusCode, isRateLimitError } from "./errors.js";
|
|
11
|
+
import { getHttpCache, versionedCacheKey } from "./http-cache.js";
|
|
12
|
+
import { probeRepoFile } from "./probe-repo-file.js";
|
|
14
13
|
/** TTL for cached anti-LLM policy scan results (1 hour). Policy docs change rarely. */
|
|
15
14
|
const POLICY_SCAN_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
16
15
|
/**
|
|
@@ -85,40 +84,13 @@ const SOURCE_FILE_FAMILIES = [
|
|
|
85
84
|
paths: ["README.md", "readme.md", "Readme.md"],
|
|
86
85
|
},
|
|
87
86
|
];
|
|
88
|
-
/**
|
|
89
|
-
* Fetch one path's raw text content. The `transient` flag distinguishes a
|
|
90
|
-
* clean miss (404 — file absent) from a degraded miss (5xx, network) so the
|
|
91
|
-
* caller can decide whether to cache "no policy" or retry. Throws on
|
|
92
|
-
* 401/auth and rate-limit per documented project error strategy.
|
|
93
|
-
*/
|
|
94
|
-
async function fetchFileText(octokit, owner, repo, path) {
|
|
95
|
-
try {
|
|
96
|
-
const { data } = await octokit.repos.getContent({ owner, repo, path });
|
|
97
|
-
if ("content" in data && typeof data.content === "string") {
|
|
98
|
-
return {
|
|
99
|
-
text: Buffer.from(data.content, "base64").toString("utf-8"),
|
|
100
|
-
transient: false,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return { text: null, transient: false };
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
const status = getHttpStatusCode(error);
|
|
107
|
-
if (status === 404)
|
|
108
|
-
return { text: null, transient: false };
|
|
109
|
-
if (status === 401 || isRateLimitError(error))
|
|
110
|
-
throw error;
|
|
111
|
-
warn(MODULE, `Unexpected error fetching ${path} from ${owner}/${repo}: ${errorMessage(error)}`);
|
|
112
|
-
return { text: null, transient: true };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
87
|
/**
|
|
116
88
|
* Fetch the first available file from a family. Probes are issued in parallel,
|
|
117
89
|
* but auth/rate-limit rejections re-throw so the IssueVetter's existing
|
|
118
90
|
* rate-limit handling kicks in instead of silently caching a wrong answer.
|
|
119
91
|
*/
|
|
120
92
|
async function fetchFamilyText(octokit, owner, repo, paths) {
|
|
121
|
-
const results = await Promise.allSettled(paths.map((p) =>
|
|
93
|
+
const results = await Promise.allSettled(paths.map((p) => probeRepoFile(octokit, owner, repo, p)));
|
|
122
94
|
let hadTransientFailure = false;
|
|
123
95
|
for (const result of results) {
|
|
124
96
|
if (result.status === "fulfilled") {
|
|
@@ -162,7 +134,7 @@ function isAntiLLMPolicyResult(value) {
|
|
|
162
134
|
*/
|
|
163
135
|
export async function fetchAndScanAntiLLMPolicy(octokit, owner, repo, options) {
|
|
164
136
|
const cache = getHttpCache();
|
|
165
|
-
const cacheKey = `anti-llm-policy:${owner}/${repo}
|
|
137
|
+
const cacheKey = versionedCacheKey(`anti-llm-policy:${owner}/${repo}`);
|
|
166
138
|
const cached = cache.getIfFresh(cacheKey, POLICY_SCAN_CACHE_TTL_MS);
|
|
167
139
|
if (isAntiLLMPolicyResult(cached))
|
|
168
140
|
return cached;
|
package/dist/core/bootstrap.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* First-run bootstrap — fetches starred repos and PR history from GitHub
|
|
3
3
|
* to seed the scout's state with the user's contribution context.
|
|
4
4
|
*/
|
|
5
|
-
import type {
|
|
5
|
+
import type { ScoutStateWriter } from "./issue-vetting.js";
|
|
6
6
|
export interface BootstrapResult {
|
|
7
7
|
starredRepoCount: number;
|
|
8
8
|
mergedPRCount: number;
|
|
@@ -12,4 +12,4 @@ export interface BootstrapResult {
|
|
|
12
12
|
skippedDueToRateLimit: boolean;
|
|
13
13
|
errors: string[];
|
|
14
14
|
}
|
|
15
|
-
export declare function bootstrapScout(scout:
|
|
15
|
+
export declare function bootstrapScout(scout: ScoutStateWriter, token: string): Promise<BootstrapResult>;
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getOctokit, checkRateLimit } from "./github.js";
|
|
6
6
|
import { debug, warn } from "./logger.js";
|
|
7
|
-
import { ConfigurationError, errorMessage,
|
|
7
|
+
import { ConfigurationError, errorMessage, rethrowIfFatal } from "./errors.js";
|
|
8
8
|
import { extractRepoFromUrl } from "./utils.js";
|
|
9
9
|
const MODULE = "bootstrap";
|
|
10
10
|
const STARRED_MAX_PAGES = 5;
|
|
@@ -51,8 +51,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
51
51
|
scout.setStarredRepos(starredRepos);
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
|
-
|
|
55
|
-
throw err;
|
|
54
|
+
rethrowIfFatal(err);
|
|
56
55
|
warn(MODULE, `Failed to fetch starred repos: ${errorMessage(err)}`);
|
|
57
56
|
errors.push("starred repos fetch failed");
|
|
58
57
|
}
|
|
@@ -83,8 +82,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
83
82
|
debug(MODULE, `Imported ${mergedPRCount} merged PRs`);
|
|
84
83
|
}
|
|
85
84
|
catch (err) {
|
|
86
|
-
|
|
87
|
-
throw err;
|
|
85
|
+
rethrowIfFatal(err);
|
|
88
86
|
warn(MODULE, `Failed to fetch merged PRs: ${errorMessage(err)}`);
|
|
89
87
|
errors.push("merged PR fetch failed");
|
|
90
88
|
}
|
|
@@ -115,8 +113,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
115
113
|
debug(MODULE, `Imported ${closedPRCount} closed PRs`);
|
|
116
114
|
}
|
|
117
115
|
catch (err) {
|
|
118
|
-
|
|
119
|
-
throw err;
|
|
116
|
+
rethrowIfFatal(err);
|
|
120
117
|
warn(MODULE, `Failed to fetch closed PRs: ${errorMessage(err)}`);
|
|
121
118
|
errors.push("closed PR fetch failed");
|
|
122
119
|
}
|
|
@@ -147,8 +144,7 @@ export async function bootstrapScout(scout, token) {
|
|
|
147
144
|
debug(MODULE, `Imported ${openPRCount} open PRs`);
|
|
148
145
|
}
|
|
149
146
|
catch (err) {
|
|
150
|
-
|
|
151
|
-
throw err;
|
|
147
|
+
rethrowIfFatal(err);
|
|
152
148
|
warn(MODULE, `Failed to fetch open PRs: ${errorMessage(err)}`);
|
|
153
149
|
errors.push("open PR fetch failed");
|
|
154
150
|
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare class ValidationError extends OssScoutError {
|
|
|
21
21
|
export declare function errorMessage(e: unknown): string;
|
|
22
22
|
export declare function getHttpStatusCode(error: unknown): number | undefined;
|
|
23
23
|
export declare function isRateLimitError(error: unknown): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Re-throw an error if it is one that must always propagate: a 401 (auth) or
|
|
26
|
+
* any rate-limit condition (429, 403 + rate-limit/abuse). Otherwise return so
|
|
27
|
+
* the caller can degrade gracefully. Centralizes the guard that was
|
|
28
|
+
* copy-pasted across ~16 catch blocks (#154).
|
|
29
|
+
*
|
|
30
|
+
* Note: catch sites that deliberately treat a rate limit as degradable use a
|
|
31
|
+
* bare `getHttpStatusCode(err) === 401` check instead and must NOT call this.
|
|
32
|
+
*/
|
|
33
|
+
export declare function rethrowIfFatal(error: unknown): void;
|
|
24
34
|
/** Error codes for JSON output. */
|
|
25
35
|
export type ErrorCode = "AUTH_REQUIRED" | "CONFIGURATION" | "NETWORK" | "NOT_FOUND" | "RATE_LIMITED" | "STATE_CORRUPTED" | "UNKNOWN" | "VALIDATION";
|
|
26
36
|
/**
|
package/dist/core/errors.js
CHANGED
|
@@ -46,10 +46,27 @@ export function isRateLimitError(error) {
|
|
|
46
46
|
return true;
|
|
47
47
|
if (status === 403) {
|
|
48
48
|
const msg = errorMessage(error).toLowerCase();
|
|
49
|
-
|
|
49
|
+
// "rate limit" also covers GitHub's "secondary rate limit" wording;
|
|
50
|
+
// abuse-detection 403s carry neither substring but are the same
|
|
51
|
+
// back-off-and-retry condition, so they must propagate too (#138).
|
|
52
|
+
return msg.includes("rate limit") || msg.includes("abuse detection");
|
|
50
53
|
}
|
|
51
54
|
return false;
|
|
52
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Re-throw an error if it is one that must always propagate: a 401 (auth) or
|
|
58
|
+
* any rate-limit condition (429, 403 + rate-limit/abuse). Otherwise return so
|
|
59
|
+
* the caller can degrade gracefully. Centralizes the guard that was
|
|
60
|
+
* copy-pasted across ~16 catch blocks (#154).
|
|
61
|
+
*
|
|
62
|
+
* Note: catch sites that deliberately treat a rate limit as degradable use a
|
|
63
|
+
* bare `getHttpStatusCode(err) === 401` check instead and must NOT call this.
|
|
64
|
+
*/
|
|
65
|
+
export function rethrowIfFatal(error) {
|
|
66
|
+
if (getHttpStatusCode(error) === 401 || isRateLimitError(error)) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
53
70
|
/**
|
|
54
71
|
* Map an unknown error to a structured ErrorCode for JSON output.
|
|
55
72
|
*/
|
|
@@ -62,10 +79,8 @@ export function resolveErrorCode(err) {
|
|
|
62
79
|
if (status === 401)
|
|
63
80
|
return "AUTH_REQUIRED";
|
|
64
81
|
if (status === 403) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return "RATE_LIMITED";
|
|
68
|
-
return "AUTH_REQUIRED";
|
|
82
|
+
// Single source of truth for the 403 rate-limit/abuse classification
|
|
83
|
+
return isRateLimitError(err) ? "RATE_LIMITED" : "AUTH_REQUIRED";
|
|
69
84
|
}
|
|
70
85
|
if (status === 404)
|
|
71
86
|
return "NOT_FOUND";
|
|
@@ -138,10 +138,22 @@ export interface DiscoverFeaturesBroadOptions {
|
|
|
138
138
|
* is independently testable without mocking the Search API.
|
|
139
139
|
*/
|
|
140
140
|
export declare function buildBroadFeatureSearchQuery(opts: {
|
|
141
|
-
|
|
141
|
+
/** Single language per query; see buildBroadFeatureSearchQueries. */
|
|
142
|
+
language?: string;
|
|
142
143
|
excludeRepos?: string[];
|
|
143
144
|
excludeOrgs?: string[];
|
|
144
145
|
}): string;
|
|
146
|
+
/**
|
|
147
|
+
* One query per language. A combined `(language:a OR language:b)` clause
|
|
148
|
+
* pushed the query past GitHub's 5-operator limit (the label ORs already
|
|
149
|
+
* spend all five), so every 2+ language config drew a 422 that the caller
|
|
150
|
+
* swallowed into "no results" (#121). "any" disables the filter.
|
|
151
|
+
*/
|
|
152
|
+
export declare function buildBroadFeatureSearchQueries(opts: {
|
|
153
|
+
languages?: string[];
|
|
154
|
+
excludeRepos?: string[];
|
|
155
|
+
excludeOrgs?: string[];
|
|
156
|
+
}): string[];
|
|
145
157
|
/**
|
|
146
158
|
* Orchestrate broad / cross-repo feature discovery (#100). Bypasses anchor
|
|
147
159
|
* resolution; runs a single GitHub Search API query for feature-labeled
|