@oss-scout/core 0.10.0 → 1.0.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 +77 -60
- package/dist/cli.js +403 -416
- 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 +7 -0
- package/dist/commands/search.js +63 -68
- 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 +4 -5
- 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 +3 -0
- package/dist/core/issue-discovery.js +51 -31
- package/dist/core/issue-eligibility.d.ts +10 -4
- package/dist/core/issue-eligibility.js +119 -67
- package/dist/core/issue-graphql.d.ts +58 -0
- package/dist/core/issue-graphql.js +108 -0
- package/dist/core/issue-vetting.d.ts +105 -8
- package/dist/core/issue-vetting.js +234 -107
- 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 +51 -18
- package/dist/core/personalization.js +101 -27
- package/dist/core/preference-fields.d.ts +47 -0
- package/dist/core/preference-fields.js +178 -0
- package/dist/core/repo-health.js +31 -15
- package/dist/core/roadmap.js +17 -3
- package/dist/core/schemas.d.ts +144 -26
- package/dist/core/schemas.js +74 -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 +0 -18
- package/dist/core/search-phases.js +27 -82
- package/dist/core/types.d.ts +146 -30
- package/dist/core/utils.js +60 -26
- 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 +59 -10
- package/dist/scout.js +244 -19
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batched GraphQL prefetch of issue "core" data (#169).
|
|
3
|
+
*
|
|
4
|
+
* `vetIssue` re-fetches each issue's basic fields (title, body, state, labels,
|
|
5
|
+
* timestamps, comment count) via a per-issue REST `issues.get`. When a search
|
|
6
|
+
* surfaces N issues that all need vetting, that is N separate REST calls before
|
|
7
|
+
* any of the deeper checks even start.
|
|
8
|
+
*
|
|
9
|
+
* `prefetchIssueCores` collapses those N calls into ONE aliased GraphQL query.
|
|
10
|
+
* The result is a map keyed by `owner/repo#number`; `vetIssue` consumes a hit
|
|
11
|
+
* instead of calling `issues.get`, and falls back to REST for any miss (a
|
|
12
|
+
* deleted issue, a permission error on one repo, or a non-fatal GraphQL blip).
|
|
13
|
+
*
|
|
14
|
+
* Scope is deliberately limited to the `issues.get` fields. The other vetting
|
|
15
|
+
* calls (timeline-based PR detection, claim scanning, project health,
|
|
16
|
+
* contribution guidelines) stay REST — batching those has pagination-semantics
|
|
17
|
+
* divergence risk and is left as a follow-up.
|
|
18
|
+
*/
|
|
19
|
+
import { rethrowIfFatal, errorMessage } from "./errors.js";
|
|
20
|
+
import { warn } from "./logger.js";
|
|
21
|
+
const MODULE = "issue-graphql";
|
|
22
|
+
/** Map key for a prefetched core, also used by callers to look one up. */
|
|
23
|
+
export function issueCoreKey(owner, repo, number) {
|
|
24
|
+
return `${owner}/${repo}#${number}`;
|
|
25
|
+
}
|
|
26
|
+
function normalizeNode(node) {
|
|
27
|
+
return {
|
|
28
|
+
id: node.databaseId,
|
|
29
|
+
title: node.title,
|
|
30
|
+
body: node.body ?? "",
|
|
31
|
+
state: node.state === "CLOSED" ? "closed" : "open",
|
|
32
|
+
labels: node.labels.nodes.map((l) => l.name),
|
|
33
|
+
commentCount: node.comments.totalCount,
|
|
34
|
+
createdAt: node.createdAt,
|
|
35
|
+
updatedAt: node.updatedAt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Batch-fetch issue core data with one aliased GraphQL query. Returns a map of
|
|
40
|
+
* `owner/repo#number` to the normalized core. Issues that the query could not
|
|
41
|
+
* resolve are simply absent — the caller is expected to fall back to REST for
|
|
42
|
+
* any key not in the map.
|
|
43
|
+
*
|
|
44
|
+
* Failure handling mirrors the rest of the vetter: fatal errors (401 / rate
|
|
45
|
+
* limit) propagate via `rethrowIfFatal`; a partial-data GraphQL error (one bad
|
|
46
|
+
* issue in the batch) keeps the aliases that did resolve; any other non-fatal
|
|
47
|
+
* error returns whatever resolved so the caller degrades to all-REST.
|
|
48
|
+
*/
|
|
49
|
+
export async function prefetchIssueCores(octokit, issues) {
|
|
50
|
+
const result = new Map();
|
|
51
|
+
if (issues.length === 0)
|
|
52
|
+
return result;
|
|
53
|
+
// Dedup by key so a repeated issue does not allocate a redundant alias.
|
|
54
|
+
const unique = [
|
|
55
|
+
...new Map(issues.map((i) => [issueCoreKey(i.owner, i.repo, i.number), i])).values(),
|
|
56
|
+
];
|
|
57
|
+
// Build a parameterized query — owner/repo/number go through GraphQL
|
|
58
|
+
// variables, never string-interpolated into the query body, so there is no
|
|
59
|
+
// injection surface even though parseGitHubUrl already validates them.
|
|
60
|
+
const varDefs = [];
|
|
61
|
+
const selections = [];
|
|
62
|
+
const variables = {};
|
|
63
|
+
unique.forEach((iss, i) => {
|
|
64
|
+
varDefs.push(`$o${i}: String!, $n${i}: String!, $num${i}: Int!`);
|
|
65
|
+
variables[`o${i}`] = iss.owner;
|
|
66
|
+
variables[`n${i}`] = iss.repo;
|
|
67
|
+
variables[`num${i}`] = iss.number;
|
|
68
|
+
selections.push(`i${i}: repository(owner: $o${i}, name: $n${i}) {
|
|
69
|
+
issue(number: $num${i}) {
|
|
70
|
+
databaseId
|
|
71
|
+
title
|
|
72
|
+
body
|
|
73
|
+
state
|
|
74
|
+
labels(first: 100) { nodes { name } }
|
|
75
|
+
comments { totalCount }
|
|
76
|
+
createdAt
|
|
77
|
+
updatedAt
|
|
78
|
+
}
|
|
79
|
+
}`);
|
|
80
|
+
});
|
|
81
|
+
const query = `query batchIssueCores(${varDefs.join(", ")}) {\n${selections.join("\n")}\n}`;
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
data = await octokit.graphql(query, variables);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
rethrowIfFatal(err);
|
|
88
|
+
// octokit's GraphqlResponseError attaches the resolved aliases to `.data`
|
|
89
|
+
// when only some issues in the batch errored (e.g. one was deleted).
|
|
90
|
+
const partial = err.data;
|
|
91
|
+
if (partial) {
|
|
92
|
+
data = partial;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
warn(MODULE, `GraphQL prefetch failed, falling back to REST: ${errorMessage(err)}`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
unique.forEach((iss, i) => {
|
|
100
|
+
const node = data?.[`i${i}`]?.issue;
|
|
101
|
+
// A null node (deleted/inaccessible) or a null databaseId leaves the key
|
|
102
|
+
// absent so the caller fetches it via REST.
|
|
103
|
+
if (!node || node.databaseId == null)
|
|
104
|
+
return;
|
|
105
|
+
result.set(issueCoreKey(iss.owner, iss.repo, iss.number), normalizeNode(node));
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - repo-health.ts — project health, contribution guidelines
|
|
8
8
|
*/
|
|
9
9
|
import { Octokit } from "@octokit/rest";
|
|
10
|
-
import { type SearchPriority, type IssueCandidate, type ProjectCategory } from "./types.js";
|
|
10
|
+
import { type SearchPriority, type IssueCandidate, type ProjectCategory, type ScoutPreferences, type ScoutState, type MergedPRRecord, type ClosedPRRecord, type OpenPRRecord } from "./types.js";
|
|
11
|
+
import { type PrefetchedIssueCore } from "./issue-graphql.js";
|
|
11
12
|
/**
|
|
12
13
|
* Feature-mode signals supplied by the caller (orchestrator) — the vetter
|
|
13
14
|
* does NOT extract these from the GitHub issue itself. When passed, they
|
|
@@ -30,6 +31,15 @@ export type FeatureSignals = {
|
|
|
30
31
|
*/
|
|
31
32
|
wontfixNoContributor?: boolean;
|
|
32
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* SLM pre-triage configuration (oss-autopilot#1122). `host` is the Ollama
|
|
36
|
+
* endpoint; an empty `host` means "use the triage default". A `null` config
|
|
37
|
+
* (not this shape) means SLM triage is disabled (#158).
|
|
38
|
+
*/
|
|
39
|
+
export interface SLMConfig {
|
|
40
|
+
model: string;
|
|
41
|
+
host: string;
|
|
42
|
+
}
|
|
33
43
|
/**
|
|
34
44
|
* Read-only interface for accessing scout state during issue vetting.
|
|
35
45
|
* Implementations may be backed by gist persistence, in-memory state, etc.
|
|
@@ -46,15 +56,89 @@ export interface ScoutStateReader {
|
|
|
46
56
|
/** Numeric quality score for a repo, or null if not evaluated. */
|
|
47
57
|
getRepoScore(repo: string): number | null;
|
|
48
58
|
/**
|
|
49
|
-
* SLM pre-triage config (oss-autopilot#1122). Returns the configured
|
|
50
|
-
*
|
|
51
|
-
*
|
|
59
|
+
* SLM pre-triage config (oss-autopilot#1122). Returns the configured model
|
|
60
|
+
* id and Ollama host, or `null` when SLM triage is not configured — vetIssue
|
|
61
|
+
* skips the SLM call on `null`. Required (#158): the old optional method with
|
|
62
|
+
* an empty-string sentinel could not distinguish "not configured" from "host
|
|
63
|
+
* defaulted"; `SLMConfig | null` makes the absence explicit.
|
|
64
|
+
*/
|
|
65
|
+
getSLMTriageConfig(): SLMConfig | null;
|
|
66
|
+
/**
|
|
67
|
+
* Number of the user's PRs closed without merge in this repo (#125).
|
|
68
|
+
* Optional so existing implementations keep compiling; absent reads as 0.
|
|
69
|
+
*/
|
|
70
|
+
getClosedWithoutMergeCount?(repo: string): number;
|
|
71
|
+
/**
|
|
72
|
+
* The configured GitHub username, used to tell the user's own in-flight PR
|
|
73
|
+
* apart from a competing one (#166). Optional; absent reads as "unknown".
|
|
74
|
+
*/
|
|
75
|
+
getGitHubUsername?(): string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Write side of the scout state, consumed by the bootstrap flow. Defined here
|
|
79
|
+
* next to ScoutStateReader so core/bootstrap.ts can depend on this contract
|
|
80
|
+
* instead of importing the OssScout facade from the package root (an upward
|
|
81
|
+
* dependency). OssScout implements it (#156).
|
|
82
|
+
*/
|
|
83
|
+
export interface ScoutStateWriter {
|
|
84
|
+
/** Read current preferences (bootstrap reads githubUsername). */
|
|
85
|
+
getPreferences(): Readonly<ScoutPreferences>;
|
|
86
|
+
/** Replace the cached starred-repo list. */
|
|
87
|
+
setStarredRepos(repos: string[]): void;
|
|
88
|
+
/** Record a merged PR (deduplicated by URL). */
|
|
89
|
+
recordMergedPR(pr: MergedPRRecord): void;
|
|
90
|
+
/** Record a PR closed without merge (deduplicated by URL). */
|
|
91
|
+
recordClosedPR(pr: ClosedPRRecord): void;
|
|
92
|
+
/** Record an open PR (deduplicated by URL). */
|
|
93
|
+
recordOpenPR(pr: OpenPRRecord): void;
|
|
94
|
+
/** Snapshot the current state (bootstrap reports counts from it). */
|
|
95
|
+
getState(): Readonly<ScoutState>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Inputs to deriveRecommendation: the already-computed check results and
|
|
99
|
+
* affinity signals. Kept as a flat record of primitives so the derivation is a
|
|
100
|
+
* pure function, independently unit-testable (#157).
|
|
101
|
+
*/
|
|
102
|
+
export interface RecommendationInput {
|
|
103
|
+
noExistingPR: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The linked PR is the user's own open PR (#166). When true, the existing-PR
|
|
106
|
+
* block is reframed as "you're already on it" — a skip with a clear reason —
|
|
107
|
+
* instead of a competing-PR penalty.
|
|
52
108
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
ownPR: boolean;
|
|
110
|
+
notClaimed: boolean;
|
|
111
|
+
clearRequirements: boolean;
|
|
112
|
+
contributionGuidelinesFound: boolean;
|
|
113
|
+
projectIsActive: boolean;
|
|
114
|
+
projectCheckFailed: boolean;
|
|
115
|
+
projectFailureReason?: string;
|
|
116
|
+
existingPRInconclusive: boolean;
|
|
117
|
+
existingPRReason?: string;
|
|
118
|
+
claimInconclusive: boolean;
|
|
119
|
+
claimReason?: string;
|
|
120
|
+
mergedCountInconclusive: boolean;
|
|
121
|
+
effectiveMergedCount: number;
|
|
122
|
+
orgName: string;
|
|
123
|
+
orgHasMergedPRs: boolean;
|
|
124
|
+
matchesCategory: boolean;
|
|
125
|
+
issueClosed: boolean;
|
|
126
|
+
/** noExistingPR && notClaimed && projectActive && clearRequirements. */
|
|
127
|
+
passedAllChecks: boolean;
|
|
57
128
|
}
|
|
129
|
+
export interface RecommendationOutput {
|
|
130
|
+
notes: string[];
|
|
131
|
+
reasonsToApprove: string[];
|
|
132
|
+
reasonsToSkip: string[];
|
|
133
|
+
recommendation: "approve" | "skip" | "needs_review";
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Derive the human-readable notes, approve/skip reasons, and the final
|
|
137
|
+
* recommendation from a vet's check results. Pure: no I/O, no state reads — the
|
|
138
|
+
* caller computes the inputs and threads them in. Extracted from vetIssue so
|
|
139
|
+
* the recommendation logic is testable in isolation (#157).
|
|
140
|
+
*/
|
|
141
|
+
export declare function deriveRecommendation(input: RecommendationInput): RecommendationOutput;
|
|
58
142
|
export declare class IssueVetter {
|
|
59
143
|
private octokit;
|
|
60
144
|
private stateReader;
|
|
@@ -70,7 +154,20 @@ export declare class IssueVetter {
|
|
|
70
154
|
*/
|
|
71
155
|
vetIssue(issueUrl: string, opts?: {
|
|
72
156
|
featureSignals?: FeatureSignals;
|
|
157
|
+
/**
|
|
158
|
+
* Issue core data already fetched in a batch GraphQL query (#169). When
|
|
159
|
+
* present it replaces the per-issue REST `issues.get`; otherwise the
|
|
160
|
+
* core is fetched via REST. The two paths are normalized to the same
|
|
161
|
+
* shape so behaviour is identical either way.
|
|
162
|
+
*/
|
|
163
|
+
prefetched?: PrefetchedIssueCore;
|
|
73
164
|
}): Promise<IssueCandidate>;
|
|
165
|
+
/**
|
|
166
|
+
* Fetch a single issue's core fields via REST and normalize them to the same
|
|
167
|
+
* shape as a GraphQL prefetch (#169). The REST fallback path when no
|
|
168
|
+
* prefetched core was supplied.
|
|
169
|
+
*/
|
|
170
|
+
private fetchIssueCore;
|
|
74
171
|
/**
|
|
75
172
|
* Vet multiple issues in parallel with concurrency limit
|
|
76
173
|
*/
|