@oss-autopilot/core 1.14.0 → 1.15.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-registry.js +2 -1
- package/dist/cli.bundle.cjs +58 -58
- package/dist/commands/daily.js +16 -25
- package/dist/commands/dashboard-data.d.ts +38 -1
- package/dist/commands/dashboard-data.js +33 -62
- package/dist/commands/dashboard-server.d.ts +10 -0
- package/dist/commands/dashboard-server.js +10 -2
- package/dist/commands/scout-bridge.js +7 -0
- package/dist/commands/vet-list.js +10 -0
- package/dist/commands/vet.js +8 -0
- package/dist/core/anti-llm-policy.d.ts +32 -0
- package/dist/core/anti-llm-policy.js +101 -0
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.js +28 -0
- package/dist/core/github-stats.js +38 -71
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/issue-grading.d.ts +75 -0
- package/dist/core/issue-grading.js +146 -0
- package/dist/core/linked-pr-classification.d.ts +30 -0
- package/dist/core/linked-pr-classification.js +53 -0
- package/dist/formatters/json.d.ts +5 -0
- package/package.json +2 -2
|
@@ -235,22 +235,18 @@ export async function fetchRecentlyMergedPRs(octokit, config, days = 7) {
|
|
|
235
235
|
};
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
* Fetch merged PRs since a watermark date for incremental storage.
|
|
240
|
-
* If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
|
|
241
|
-
* Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
|
|
242
|
-
*/
|
|
243
|
-
export async function fetchMergedPRsSince(octokit, config, since) {
|
|
238
|
+
async function fetchPRsSince(octokit, config, adapter, since) {
|
|
244
239
|
if (!config.githubUsername) {
|
|
245
|
-
warn(MODULE,
|
|
240
|
+
warn(MODULE, `Skipping ${adapter.kind} PRs fetch: no githubUsername configured.`);
|
|
246
241
|
return [];
|
|
247
242
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
debug(MODULE, `Fetching merged PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
243
|
+
const q = adapter.buildQuery(config.githubUsername, since);
|
|
244
|
+
debug(MODULE, `Fetching ${adapter.kind} PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
251
245
|
const results = [];
|
|
252
246
|
let page = 1;
|
|
253
247
|
let fetched = 0;
|
|
248
|
+
// Populated from the first Search API response; always set before we exit the loop.
|
|
249
|
+
let totalCount;
|
|
254
250
|
while (true) {
|
|
255
251
|
const { data } = await octokit.search.issuesAndPullRequests({
|
|
256
252
|
q,
|
|
@@ -259,34 +255,48 @@ export async function fetchMergedPRsSince(octokit, config, since) {
|
|
|
259
255
|
per_page: 100,
|
|
260
256
|
page,
|
|
261
257
|
});
|
|
258
|
+
totalCount = data.total_count;
|
|
262
259
|
for (const item of data.items) {
|
|
263
260
|
const parsed = parseGitHubUrl(item.html_url);
|
|
264
261
|
if (!parsed) {
|
|
265
|
-
warn(MODULE, `Skipping
|
|
262
|
+
warn(MODULE, `Skipping ${adapter.kind} PR with unparseable URL: ${item.html_url}`);
|
|
266
263
|
continue;
|
|
267
264
|
}
|
|
268
265
|
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
269
266
|
continue;
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
272
|
-
warn(MODULE, `Skipping
|
|
267
|
+
const date = adapter.extractDate(item);
|
|
268
|
+
if (!date) {
|
|
269
|
+
warn(MODULE, `Skipping ${adapter.kind} PR with no ${adapter.dateNoun} date: ${item.html_url}`);
|
|
273
270
|
continue;
|
|
274
271
|
}
|
|
275
|
-
results.push(
|
|
276
|
-
url: item.html_url,
|
|
277
|
-
title: item.title,
|
|
278
|
-
mergedAt,
|
|
279
|
-
});
|
|
272
|
+
results.push(adapter.buildRecord(item, date));
|
|
280
273
|
}
|
|
281
274
|
fetched += data.items.length;
|
|
282
|
-
if (fetched >=
|
|
275
|
+
if (fetched >= totalCount || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
283
276
|
break;
|
|
284
277
|
}
|
|
285
278
|
page++;
|
|
286
279
|
}
|
|
287
|
-
|
|
280
|
+
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
281
|
+
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} ${adapter.kind} PRs. Oldest PRs may be missing.`);
|
|
282
|
+
}
|
|
283
|
+
debug(MODULE, `Fetched ${results.length} ${adapter.kind} PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
288
284
|
return results;
|
|
289
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Fetch merged PRs since a watermark date for incremental storage.
|
|
288
|
+
* If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
|
|
289
|
+
* Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
|
|
290
|
+
*/
|
|
291
|
+
export async function fetchMergedPRsSince(octokit, config, since) {
|
|
292
|
+
return fetchPRsSince(octokit, config, {
|
|
293
|
+
kind: 'merged',
|
|
294
|
+
dateNoun: 'merge',
|
|
295
|
+
buildQuery: (u, s) => `is:pr is:merged author:${u} -user:${u}${s ? ` merged:>${s}` : ''}`,
|
|
296
|
+
extractDate: (item) => item.pull_request?.merged_at || item.closed_at || '',
|
|
297
|
+
buildRecord: (item, date) => ({ url: item.html_url, title: item.title, mergedAt: date }),
|
|
298
|
+
}, since);
|
|
299
|
+
}
|
|
290
300
|
/**
|
|
291
301
|
* Fetch closed-without-merge PRs since a watermark date for incremental storage.
|
|
292
302
|
* If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
|
|
@@ -294,54 +304,11 @@ export async function fetchMergedPRsSince(octokit, config, since) {
|
|
|
294
304
|
* Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
|
|
295
305
|
*/
|
|
296
306
|
export async function fetchClosedPRsSince(octokit, config, since) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const results = [];
|
|
305
|
-
let page = 1;
|
|
306
|
-
let fetched = 0;
|
|
307
|
-
let totalCount;
|
|
308
|
-
while (true) {
|
|
309
|
-
const { data } = await octokit.search.issuesAndPullRequests({
|
|
310
|
-
q,
|
|
311
|
-
sort: 'updated',
|
|
312
|
-
order: 'desc',
|
|
313
|
-
per_page: 100,
|
|
314
|
-
page,
|
|
315
|
-
});
|
|
316
|
-
totalCount = data.total_count;
|
|
317
|
-
for (const item of data.items) {
|
|
318
|
-
const parsed = parseGitHubUrl(item.html_url);
|
|
319
|
-
if (!parsed) {
|
|
320
|
-
warn(MODULE, `Skipping closed PR with unparseable URL: ${item.html_url}`);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
324
|
-
continue;
|
|
325
|
-
const closedAt = item.closed_at || '';
|
|
326
|
-
if (!closedAt) {
|
|
327
|
-
warn(MODULE, `Skipping closed PR with no close date: ${item.html_url}`);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
results.push({
|
|
331
|
-
url: item.html_url,
|
|
332
|
-
title: item.title,
|
|
333
|
-
closedAt,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
fetched += data.items.length;
|
|
337
|
-
if (fetched >= totalCount || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
page++;
|
|
341
|
-
}
|
|
342
|
-
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
343
|
-
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} closed PRs. Oldest PRs may be missing.`);
|
|
344
|
-
}
|
|
345
|
-
debug(MODULE, `Fetched ${results.length} closed PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
346
|
-
return results;
|
|
307
|
+
return fetchPRsSince(octokit, config, {
|
|
308
|
+
kind: 'closed',
|
|
309
|
+
dateNoun: 'close',
|
|
310
|
+
buildQuery: (u, s) => `is:pr is:closed is:unmerged author:${u} -user:${u}${s ? ` closed:>${s}` : ''}`,
|
|
311
|
+
extractDate: (item) => item.closed_at || '',
|
|
312
|
+
buildRecord: (item, date) => ({ url: item.html_url, title: item.title, closedAt: date }),
|
|
313
|
+
}, since);
|
|
347
314
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -9,11 +9,13 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
9
9
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
10
10
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
11
11
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
12
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
12
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
17
17
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
18
|
+
export { classifyLinkedPR, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
|
|
19
|
+
export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type AntiLLMScanResult, } from './anti-llm-policy.js';
|
|
18
20
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
|
|
19
21
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -10,11 +10,13 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
10
10
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
11
11
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
12
12
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
13
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
13
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
|
|
14
14
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
15
15
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
16
16
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
17
17
|
export { computeContributionStats } from './stats.js';
|
|
18
18
|
export { fetchPRTemplate } from './pr-template.js';
|
|
19
|
+
export { classifyLinkedPR, } from './linked-pr-classification.js';
|
|
20
|
+
export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
|
|
19
21
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
|
|
20
22
|
export * from './types.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue success-likelihood grade (#858).
|
|
3
|
+
*
|
|
4
|
+
* Predicts the probability that a contribution to a given repo will be
|
|
5
|
+
* accepted and merged, using signals already collected during vetting.
|
|
6
|
+
*
|
|
7
|
+
* The grade is letter-based (A/B/C/F — no D). Each signal is graded
|
|
8
|
+
* independently; the overall grade is the worst of the three, and is
|
|
9
|
+
* further degraded one step if any signal is unknown (missing data is
|
|
10
|
+
* treated as a risk, not ignored). This matches the policy previously
|
|
11
|
+
* described as prose in agents/issue-scout.md.
|
|
12
|
+
*/
|
|
13
|
+
export type GradeLetter = 'A' | 'B' | 'C' | 'F';
|
|
14
|
+
export interface GradeSignals {
|
|
15
|
+
/** Average maintainer response time in days; null if unknown. */
|
|
16
|
+
avgResponseDays: number | null;
|
|
17
|
+
/** Fraction of recent PRs that merged (0–1); null if unknown. */
|
|
18
|
+
mergeRate: number | null;
|
|
19
|
+
/** Days since the most recent commit on the default branch; null if unknown. */
|
|
20
|
+
daysSinceLastCommit: number | null;
|
|
21
|
+
}
|
|
22
|
+
export interface GradeResult {
|
|
23
|
+
letter: GradeLetter;
|
|
24
|
+
/** Short human-readable explanation of what drove the grade. */
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build `GradeSignals` from a vet candidate's project health plus the
|
|
29
|
+
* optional autopilot-tracked repo score.
|
|
30
|
+
*
|
|
31
|
+
* Notes on each signal:
|
|
32
|
+
*
|
|
33
|
+
* - `avgResponseDays` — `@oss-scout/core`'s `projectHealth.avgIssueResponseDays`
|
|
34
|
+
* is a hardcoded `0` placeholder (it doesn't yet make the additional API
|
|
35
|
+
* calls needed to compute a real value). We therefore prefer
|
|
36
|
+
* `repoScore.avgResponseDays`, which autopilot derives from its own PR
|
|
37
|
+
* tracking, and fall back to `null` (unknown) if neither source has a
|
|
38
|
+
* usable value.
|
|
39
|
+
* - `mergeRate` — derived from repoScore counts. Requires a non-empty PR
|
|
40
|
+
* history; `0/0` is `null`, not `0`.
|
|
41
|
+
* - `daysSinceLastCommit` — taken from scout's `projectHealth`, but only
|
|
42
|
+
* when scout's health check succeeded (`checkFailed` means scout filled
|
|
43
|
+
* in sentinels and shouldn't be trusted).
|
|
44
|
+
*/
|
|
45
|
+
export declare function deriveGradeSignals(params: {
|
|
46
|
+
projectHealth: {
|
|
47
|
+
avgIssueResponseDays: number | null;
|
|
48
|
+
daysSinceLastCommit: number | null;
|
|
49
|
+
checkFailed?: boolean;
|
|
50
|
+
};
|
|
51
|
+
repoScore: {
|
|
52
|
+
mergedPRCount: number;
|
|
53
|
+
closedWithoutMergeCount: number;
|
|
54
|
+
avgResponseDays?: number | null;
|
|
55
|
+
} | null;
|
|
56
|
+
}): GradeSignals;
|
|
57
|
+
/**
|
|
58
|
+
* End-to-end helper for vet callers: reads the repo score, derives
|
|
59
|
+
* signals from a scout candidate, and returns the grade. Callers pass
|
|
60
|
+
* the `projectHealth` straight through from `scout.vetIssue()`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function gradeFromCandidate(params: {
|
|
63
|
+
repo: string;
|
|
64
|
+
projectHealth: {
|
|
65
|
+
avgIssueResponseDays: number | null;
|
|
66
|
+
daysSinceLastCommit: number | null;
|
|
67
|
+
checkFailed?: boolean;
|
|
68
|
+
};
|
|
69
|
+
getRepoScore: (repo: string) => {
|
|
70
|
+
mergedPRCount: number;
|
|
71
|
+
closedWithoutMergeCount: number;
|
|
72
|
+
avgResponseDays: number | null;
|
|
73
|
+
} | undefined;
|
|
74
|
+
}): GradeResult;
|
|
75
|
+
export declare function computeSuccessGrade(signals: GradeSignals): GradeResult;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue success-likelihood grade (#858).
|
|
3
|
+
*
|
|
4
|
+
* Predicts the probability that a contribution to a given repo will be
|
|
5
|
+
* accepted and merged, using signals already collected during vetting.
|
|
6
|
+
*
|
|
7
|
+
* The grade is letter-based (A/B/C/F — no D). Each signal is graded
|
|
8
|
+
* independently; the overall grade is the worst of the three, and is
|
|
9
|
+
* further degraded one step if any signal is unknown (missing data is
|
|
10
|
+
* treated as a risk, not ignored). This matches the policy previously
|
|
11
|
+
* described as prose in agents/issue-scout.md.
|
|
12
|
+
*/
|
|
13
|
+
const SEVERITY = { A: 0, B: 1, C: 2, F: 3 };
|
|
14
|
+
const LETTERS = ['A', 'B', 'C', 'F'];
|
|
15
|
+
function worst(grades) {
|
|
16
|
+
return grades.reduce((acc, g) => (SEVERITY[g.letter] > SEVERITY[acc.letter] ? g : acc));
|
|
17
|
+
}
|
|
18
|
+
function degradeOneStep(letter) {
|
|
19
|
+
return LETTERS[Math.min(SEVERITY[letter] + 1, LETTERS.length - 1)];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Coerce a candidate numeric signal to `number` if it's finite and in
|
|
23
|
+
* the given range, otherwise `null`. Non-finite, NaN, and out-of-range
|
|
24
|
+
* values are treated as unknown (and therefore trigger the degrade
|
|
25
|
+
* rule) rather than silently producing bogus grades from garbage input.
|
|
26
|
+
*/
|
|
27
|
+
function sanitize(value, min, max) {
|
|
28
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
29
|
+
return null;
|
|
30
|
+
if (value < min || value > max)
|
|
31
|
+
return null;
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function gradeResponsiveness(avgResponseDays) {
|
|
35
|
+
if (avgResponseDays < 3)
|
|
36
|
+
return { letter: 'A', detail: `~${Math.round(avgResponseDays)}-day avg response` };
|
|
37
|
+
if (avgResponseDays < 14)
|
|
38
|
+
return { letter: 'B', detail: `${Math.round(avgResponseDays)}-day avg response` };
|
|
39
|
+
if (avgResponseDays <= 60)
|
|
40
|
+
return { letter: 'C', detail: `${Math.round(avgResponseDays)}-day avg response` };
|
|
41
|
+
return { letter: 'F', detail: 'unresponsive maintainers' };
|
|
42
|
+
}
|
|
43
|
+
function gradeMergeRate(mergeRate) {
|
|
44
|
+
const pct = Math.round(mergeRate * 100);
|
|
45
|
+
if (mergeRate > 0.7)
|
|
46
|
+
return { letter: 'A', detail: `merges ${pct}% of PRs` };
|
|
47
|
+
if (mergeRate >= 0.4)
|
|
48
|
+
return { letter: 'B', detail: `merges ${pct}% of PRs` };
|
|
49
|
+
if (mergeRate >= 0.1)
|
|
50
|
+
return { letter: 'C', detail: `merges ${pct}% of PRs` };
|
|
51
|
+
return { letter: 'F', detail: `merges ${pct}% of PRs` };
|
|
52
|
+
}
|
|
53
|
+
function gradeActivity(daysSinceLastCommit) {
|
|
54
|
+
if (daysSinceLastCommit < 7)
|
|
55
|
+
return { letter: 'A', detail: 'commits in last week' };
|
|
56
|
+
if (daysSinceLastCommit < 30)
|
|
57
|
+
return { letter: 'B', detail: 'commits in last month' };
|
|
58
|
+
if (daysSinceLastCommit < 90)
|
|
59
|
+
return { letter: 'C', detail: 'commits in last 90 days' };
|
|
60
|
+
return { letter: 'F', detail: `no commits in ${daysSinceLastCommit}+ days` };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build `GradeSignals` from a vet candidate's project health plus the
|
|
64
|
+
* optional autopilot-tracked repo score.
|
|
65
|
+
*
|
|
66
|
+
* Notes on each signal:
|
|
67
|
+
*
|
|
68
|
+
* - `avgResponseDays` — `@oss-scout/core`'s `projectHealth.avgIssueResponseDays`
|
|
69
|
+
* is a hardcoded `0` placeholder (it doesn't yet make the additional API
|
|
70
|
+
* calls needed to compute a real value). We therefore prefer
|
|
71
|
+
* `repoScore.avgResponseDays`, which autopilot derives from its own PR
|
|
72
|
+
* tracking, and fall back to `null` (unknown) if neither source has a
|
|
73
|
+
* usable value.
|
|
74
|
+
* - `mergeRate` — derived from repoScore counts. Requires a non-empty PR
|
|
75
|
+
* history; `0/0` is `null`, not `0`.
|
|
76
|
+
* - `daysSinceLastCommit` — taken from scout's `projectHealth`, but only
|
|
77
|
+
* when scout's health check succeeded (`checkFailed` means scout filled
|
|
78
|
+
* in sentinels and shouldn't be trusted).
|
|
79
|
+
*/
|
|
80
|
+
export function deriveGradeSignals(params) {
|
|
81
|
+
const { projectHealth, repoScore } = params;
|
|
82
|
+
const healthTrusted = !projectHealth.checkFailed;
|
|
83
|
+
const avgResponseDays = sanitize(repoScore?.avgResponseDays, 0, Number.MAX_SAFE_INTEGER) ??
|
|
84
|
+
(healthTrusted ? sanitize(projectHealth.avgIssueResponseDays, 0.001, Number.MAX_SAFE_INTEGER) : null);
|
|
85
|
+
const daysSinceLastCommit = healthTrusted
|
|
86
|
+
? sanitize(projectHealth.daysSinceLastCommit, 0, Number.MAX_SAFE_INTEGER)
|
|
87
|
+
: null;
|
|
88
|
+
let mergeRate = null;
|
|
89
|
+
if (repoScore) {
|
|
90
|
+
const merged = sanitize(repoScore.mergedPRCount, 0, Number.MAX_SAFE_INTEGER);
|
|
91
|
+
const closed = sanitize(repoScore.closedWithoutMergeCount, 0, Number.MAX_SAFE_INTEGER);
|
|
92
|
+
if (merged !== null && closed !== null) {
|
|
93
|
+
const total = merged + closed;
|
|
94
|
+
mergeRate = total === 0 ? null : merged / total;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { avgResponseDays, mergeRate, daysSinceLastCommit };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* End-to-end helper for vet callers: reads the repo score, derives
|
|
101
|
+
* signals from a scout candidate, and returns the grade. Callers pass
|
|
102
|
+
* the `projectHealth` straight through from `scout.vetIssue()`.
|
|
103
|
+
*/
|
|
104
|
+
export function gradeFromCandidate(params) {
|
|
105
|
+
const repoScore = params.getRepoScore(params.repo);
|
|
106
|
+
return computeSuccessGrade(deriveGradeSignals({
|
|
107
|
+
projectHealth: params.projectHealth,
|
|
108
|
+
repoScore: repoScore
|
|
109
|
+
? {
|
|
110
|
+
mergedPRCount: repoScore.mergedPRCount,
|
|
111
|
+
closedWithoutMergeCount: repoScore.closedWithoutMergeCount,
|
|
112
|
+
avgResponseDays: repoScore.avgResponseDays,
|
|
113
|
+
}
|
|
114
|
+
: null,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
export function computeSuccessGrade(signals) {
|
|
118
|
+
const graded = [];
|
|
119
|
+
const unknowns = [];
|
|
120
|
+
const resp = sanitize(signals.avgResponseDays, 0, Number.MAX_SAFE_INTEGER);
|
|
121
|
+
if (resp === null)
|
|
122
|
+
unknowns.push('maintainer responsiveness');
|
|
123
|
+
else
|
|
124
|
+
graded.push(gradeResponsiveness(resp));
|
|
125
|
+
const mr = sanitize(signals.mergeRate, 0, 1);
|
|
126
|
+
if (mr === null)
|
|
127
|
+
unknowns.push('merge rate');
|
|
128
|
+
else
|
|
129
|
+
graded.push(gradeMergeRate(mr));
|
|
130
|
+
const act = sanitize(signals.daysSinceLastCommit, 0, Number.MAX_SAFE_INTEGER);
|
|
131
|
+
if (act === null)
|
|
132
|
+
unknowns.push('activity');
|
|
133
|
+
else
|
|
134
|
+
graded.push(gradeActivity(act));
|
|
135
|
+
// No signal available at all — give F so callers see missing data as a
|
|
136
|
+
// strong negative rather than a neutral grade.
|
|
137
|
+
if (graded.length === 0) {
|
|
138
|
+
return { letter: 'F', reason: `unknown ${unknowns.join(', ')}` };
|
|
139
|
+
}
|
|
140
|
+
const worstKnown = worst(graded);
|
|
141
|
+
const finalLetter = unknowns.length > 0 ? degradeOneStep(worstKnown.letter) : worstKnown.letter;
|
|
142
|
+
const reasonParts = [worstKnown.detail];
|
|
143
|
+
if (unknowns.length > 0)
|
|
144
|
+
reasonParts.push(`unknown ${unknowns.join(', ')}`);
|
|
145
|
+
return { letter: finalLetter, reason: reasonParts.join(', ') };
|
|
146
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linked-PR classification (#910, #978).
|
|
3
|
+
*
|
|
4
|
+
* Given the first linked PR on an issue, decide how it affects whether
|
|
5
|
+
* the issue is actionable for the current user. Previously described as
|
|
6
|
+
* prose in agents/issue-scout.md; extracted here as a pure function so
|
|
7
|
+
* the classification is unit-testable and uniform across callers.
|
|
8
|
+
*
|
|
9
|
+
* Integration with the vet flow is deferred — scout does not yet surface
|
|
10
|
+
* linked-PR metadata on `IssueCandidate`, so consumers need to fetch the
|
|
11
|
+
* linked PR themselves (e.g. via Octokit) and hand the shape to this
|
|
12
|
+
* function. See #978 for the upstream data-contract work.
|
|
13
|
+
*/
|
|
14
|
+
export type LinkedPRClassification = 'none' | 'user_open' | 'user_closed' | 'user_merged' | 'other_open' | 'other_closed' | 'other_merged';
|
|
15
|
+
export type LinkedPRState = 'open' | 'closed' | 'merged';
|
|
16
|
+
export interface LinkedPR {
|
|
17
|
+
/**
|
|
18
|
+
* May be `null` for deleted GitHub accounts ("ghost" users); the
|
|
19
|
+
* declared type on GitHub's API allows null here even though the REST
|
|
20
|
+
* schema example typically shows a populated user.
|
|
21
|
+
*/
|
|
22
|
+
author: {
|
|
23
|
+
login: string;
|
|
24
|
+
} | null;
|
|
25
|
+
state: LinkedPRState;
|
|
26
|
+
}
|
|
27
|
+
export declare function classifyLinkedPR(params: {
|
|
28
|
+
linkedPR: LinkedPR | null;
|
|
29
|
+
userLogin: string;
|
|
30
|
+
}): LinkedPRClassification;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linked-PR classification (#910, #978).
|
|
3
|
+
*
|
|
4
|
+
* Given the first linked PR on an issue, decide how it affects whether
|
|
5
|
+
* the issue is actionable for the current user. Previously described as
|
|
6
|
+
* prose in agents/issue-scout.md; extracted here as a pure function so
|
|
7
|
+
* the classification is unit-testable and uniform across callers.
|
|
8
|
+
*
|
|
9
|
+
* Integration with the vet flow is deferred — scout does not yet surface
|
|
10
|
+
* linked-PR metadata on `IssueCandidate`, so consumers need to fetch the
|
|
11
|
+
* linked PR themselves (e.g. via Octokit) and hand the shape to this
|
|
12
|
+
* function. See #978 for the upstream data-contract work.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a state value from either REST (lowercase `open`/`closed`
|
|
16
|
+
* plus a separate `merged` boolean) or GraphQL (uppercase `OPEN`/
|
|
17
|
+
* `CLOSED`/`MERGED` union) into our internal lowercase form. Callers
|
|
18
|
+
* converting from REST should pre-mix `merged` into the state before
|
|
19
|
+
* calling; see the tests for expected shapes.
|
|
20
|
+
*/
|
|
21
|
+
function normalizeState(state) {
|
|
22
|
+
const lower = state.toLowerCase();
|
|
23
|
+
if (lower === 'open' || lower === 'closed' || lower === 'merged')
|
|
24
|
+
return lower;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function classifyLinkedPR(params) {
|
|
28
|
+
const { linkedPR, userLogin } = params;
|
|
29
|
+
if (!linkedPR)
|
|
30
|
+
return 'none';
|
|
31
|
+
const state = normalizeState(linkedPR.state);
|
|
32
|
+
// Unknown state (e.g., future-extended values) is safest to treat as
|
|
33
|
+
// "closed" — skip-worthy but non-fatal — rather than throwing and
|
|
34
|
+
// breaking the whole vetting pipeline on one malformed payload.
|
|
35
|
+
const effectiveState = state ?? 'closed';
|
|
36
|
+
// GitHub usernames are case-insensitive ASCII. Ghost authors (deleted
|
|
37
|
+
// accounts) return `null` or an empty login; in both cases we can't
|
|
38
|
+
// prove the PR is the user's own, so we classify it as "other_*".
|
|
39
|
+
const authorLogin = linkedPR.author?.login ?? '';
|
|
40
|
+
const isUserOwn = authorLogin !== '' && userLogin !== '' && authorLogin.toLowerCase() === userLogin.toLowerCase();
|
|
41
|
+
if (isUserOwn) {
|
|
42
|
+
if (effectiveState === 'open')
|
|
43
|
+
return 'user_open';
|
|
44
|
+
if (effectiveState === 'merged')
|
|
45
|
+
return 'user_merged';
|
|
46
|
+
return 'user_closed';
|
|
47
|
+
}
|
|
48
|
+
if (effectiveState === 'open')
|
|
49
|
+
return 'other_open';
|
|
50
|
+
if (effectiveState === 'merged')
|
|
51
|
+
return 'other_merged';
|
|
52
|
+
return 'other_closed';
|
|
53
|
+
}
|
|
@@ -313,6 +313,11 @@ export interface VetOutput {
|
|
|
313
313
|
reasonsToSkip: string[];
|
|
314
314
|
projectHealth: unknown;
|
|
315
315
|
vettingResult: unknown;
|
|
316
|
+
/** Success-likelihood grade (#858): predicts whether a PR will merge. */
|
|
317
|
+
grade: {
|
|
318
|
+
letter: 'A' | 'B' | 'C' | 'F';
|
|
319
|
+
reason: string;
|
|
320
|
+
};
|
|
316
321
|
}
|
|
317
322
|
/** Output of the comments command */
|
|
318
323
|
export interface CommentsOutput {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "CLI and core library for managing open source contributions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@octokit/plugin-throttling": "^11.0.3",
|
|
52
52
|
"@octokit/rest": "^22.0.1",
|
|
53
|
-
"@oss-scout/core": "^0.
|
|
53
|
+
"@oss-scout/core": "^0.5.0",
|
|
54
54
|
"commander": "^14.0.3",
|
|
55
55
|
"zod": "^4.3.6"
|
|
56
56
|
},
|