@oss-autopilot/core 3.10.0 → 3.12.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-registry.d.ts +7 -0
- package/dist/cli-registry.js +58 -5
- package/dist/cli.bundle.cjs +165 -112
- package/dist/cli.js +11 -3
- package/dist/commands/comments.js +31 -15
- package/dist/commands/compliance-score.js +12 -4
- package/dist/commands/daily-render.d.ts +2 -1
- package/dist/commands/daily-render.js +8 -2
- package/dist/commands/daily.d.ts +3 -1
- package/dist/commands/daily.js +54 -4
- package/dist/commands/dashboard-data.d.ts +17 -0
- package/dist/commands/dashboard-data.js +62 -4
- package/dist/commands/dashboard-server.js +100 -26
- package/dist/commands/dismiss.d.ts +4 -0
- package/dist/commands/dismiss.js +4 -4
- package/dist/commands/guidelines.d.ts +19 -0
- package/dist/commands/guidelines.js +23 -4
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/list-move-tier.d.ts +11 -3
- package/dist/commands/list-move-tier.js +18 -7
- package/dist/commands/move.d.ts +2 -0
- package/dist/commands/move.js +12 -8
- package/dist/commands/repo-vet.js +30 -8
- package/dist/commands/search.js +17 -3
- package/dist/commands/shelve.d.ts +4 -0
- package/dist/commands/shelve.js +4 -4
- package/dist/commands/verify-issue.d.ts +20 -0
- package/dist/commands/verify-issue.js +32 -0
- package/dist/core/daily-logic.js +65 -52
- package/dist/core/gist-state-store.js +42 -7
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/issue-conversation.js +15 -2
- package/dist/core/issue-verification.d.ts +91 -0
- package/dist/core/issue-verification.js +270 -0
- package/dist/core/paths.d.ts +12 -0
- package/dist/core/paths.js +16 -0
- package/dist/core/pr-attention.d.ts +52 -0
- package/dist/core/pr-attention.js +76 -0
- package/dist/core/pr-comments-fetcher.d.ts +10 -2
- package/dist/core/pr-comments-fetcher.js +22 -4
- package/dist/core/state-persistence.d.ts +31 -9
- package/dist/core/state-persistence.js +51 -16
- package/dist/core/state.d.ts +18 -1
- package/dist/core/state.js +35 -3
- package/dist/core/types.d.ts +7 -0
- package/dist/core/untrusted-content.d.ts +24 -3
- package/dist/core/untrusted-content.js +31 -3
- package/dist/formatters/json.d.ts +83 -2
- package/dist/formatters/json.js +55 -1
- package/package.json +7 -7
package/dist/core/daily-logic.js
CHANGED
|
@@ -248,62 +248,75 @@ export function collectActionableIssues(prs, lastDigestAt) {
|
|
|
248
248
|
'merge_conflict',
|
|
249
249
|
'incomplete_checklist',
|
|
250
250
|
];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
251
|
+
// #1352: every needs_addressing PR produces exactly one entry, including
|
|
252
|
+
// PRs whose actionReason is missing or outside reasonOrder (the defensive
|
|
253
|
+
// default below). This keeps the CLI brief's count equal to the dashboard's
|
|
254
|
+
// needs_attention bucket by construction — previously an unmapped reason
|
|
255
|
+
// silently dropped the PR from the brief while the dashboard still counted
|
|
256
|
+
// it. Unmapped reasons sort last.
|
|
257
|
+
const sortedPRs = [...actionPRs].sort((a, b) => {
|
|
258
|
+
const rank = (pr) => {
|
|
259
|
+
const i = reasonOrder.indexOf(pr.actionReason);
|
|
260
|
+
return i === -1 ? reasonOrder.length : i;
|
|
261
|
+
};
|
|
262
|
+
return rank(a) - rank(b);
|
|
263
|
+
});
|
|
264
|
+
for (const pr of sortedPRs) {
|
|
265
|
+
if (pr.actionReason === undefined) {
|
|
266
|
+
warn('daily-logic', `needs_addressing PR ${pr.url} has no actionReason — defaulting to needs_response`);
|
|
267
|
+
}
|
|
268
|
+
const reason = pr.actionReason ?? 'needs_response';
|
|
269
|
+
let label;
|
|
270
|
+
let type;
|
|
271
|
+
switch (reason) {
|
|
272
|
+
case 'needs_response': {
|
|
273
|
+
label = '[Needs Response]';
|
|
274
|
+
type = 'needs_response';
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'needs_changes': {
|
|
278
|
+
label = '[Needs Changes]';
|
|
279
|
+
type = 'needs_changes';
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case 'failing_ci': {
|
|
283
|
+
const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
|
|
284
|
+
label = `[CI Failing${checkInfo}]`;
|
|
285
|
+
type = 'ci_failing';
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case 'merge_conflict': {
|
|
289
|
+
label = '[Merge Conflict]';
|
|
290
|
+
type = 'merge_conflict';
|
|
291
|
+
break;
|
|
293
292
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
|
|
300
|
-
isNewContribution = true;
|
|
293
|
+
case 'incomplete_checklist': {
|
|
294
|
+
const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
|
|
295
|
+
label = `[Incomplete Checklist${stats}]`;
|
|
296
|
+
type = 'incomplete_checklist';
|
|
297
|
+
break;
|
|
301
298
|
}
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
default: {
|
|
300
|
+
// Defensive fallback for ActionReason values not explicitly handled
|
|
301
|
+
// above (e.g. ci_not_running, needs_rebase, missing_required_files).
|
|
302
|
+
// These aren't in reasonOrder today but this guards future additions.
|
|
303
|
+
warn('daily-logic', `Unhandled ActionReason "${reason}" for PR ${pr.url} — falling back to needs_response`);
|
|
304
|
+
label = `[${reason}]`;
|
|
305
|
+
type = 'needs_response';
|
|
304
306
|
}
|
|
305
|
-
issues.push({ type, pr, label, isNewContribution });
|
|
306
307
|
}
|
|
308
|
+
// A PR is "new" if it was created after the last daily digest (first time seen).
|
|
309
|
+
// If there's no previous digest (first run) or createdAt is invalid, assume new.
|
|
310
|
+
const createdTime = new Date(pr.createdAt).getTime();
|
|
311
|
+
let isNewContribution;
|
|
312
|
+
if (isNaN(createdTime)) {
|
|
313
|
+
warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
|
|
314
|
+
isNewContribution = true;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
isNewContribution = isNaN(lastDigestTime) || createdTime > lastDigestTime;
|
|
318
|
+
}
|
|
319
|
+
issues.push({ type, pr, label, isNewContribution });
|
|
307
320
|
}
|
|
308
321
|
return issues;
|
|
309
322
|
}
|
|
@@ -49,7 +49,7 @@ import { AgentStateSchema } from './state-schema.js';
|
|
|
49
49
|
import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3, migrateV3ToV4, } from './state-persistence.js';
|
|
50
50
|
import { getGistIdPath, getStateCachePath } from './paths.js';
|
|
51
51
|
import { debug, warn } from './logger.js';
|
|
52
|
-
import { GistPermissionError, GistConcurrencyError, GistCorruptError, isRateLimitError } from './errors.js';
|
|
52
|
+
import { ConfigurationError, GistPermissionError, GistConcurrencyError, GistCorruptError, isRateLimitError, } from './errors.js';
|
|
53
53
|
const MODULE = 'gist-store';
|
|
54
54
|
/**
|
|
55
55
|
* Extract the ETag header from an Octokit response, tolerating both lower-
|
|
@@ -139,6 +139,13 @@ export class GistStateStore {
|
|
|
139
139
|
return { gistId: localId, state, created: false };
|
|
140
140
|
}
|
|
141
141
|
catch (err) {
|
|
142
|
+
// A corrupt or permission-broken Gist must surface immediately
|
|
143
|
+
// (#1367): falling through to search would either re-find the same
|
|
144
|
+
// corrupt Gist or silently abandon it and create a fresh one.
|
|
145
|
+
if (err instanceof ConfigurationError)
|
|
146
|
+
throw err;
|
|
147
|
+
if (isRateLimitError(err))
|
|
148
|
+
throw err;
|
|
142
149
|
warn(MODULE, `Failed to fetch Gist ${localId}, will search/create`, err);
|
|
143
150
|
// Fall through to search
|
|
144
151
|
}
|
|
@@ -159,10 +166,22 @@ export class GistStateStore {
|
|
|
159
166
|
return { gistId: id, state, created: true };
|
|
160
167
|
}
|
|
161
168
|
catch (err) {
|
|
162
|
-
// Configuration errors (
|
|
163
|
-
|
|
169
|
+
// Configuration errors (GistPermissionError, GistCorruptError) and rate
|
|
170
|
+
// limits must surface, not degrade (#1367). A corrupt Gist especially:
|
|
171
|
+
// fetchAndCache arms this.gistId/lastFetchedEtag before the parse
|
|
172
|
+
// throws, so a degraded store could push() local or fresh state over
|
|
173
|
+
// the corrupt remote — the exact data loss #1201 exists to prevent.
|
|
174
|
+
// Rate limits propagate per the errors.ts contract; degrading would
|
|
175
|
+
// present "you are rate-limited" as a stale local cache.
|
|
176
|
+
if (err instanceof ConfigurationError)
|
|
164
177
|
throw err;
|
|
165
|
-
|
|
178
|
+
if (isRateLimitError(err))
|
|
179
|
+
throw err;
|
|
180
|
+
// All API paths failed — enter degraded mode. Disarm the remote write
|
|
181
|
+
// path first: a degraded store never verified its Gist, so it must
|
|
182
|
+
// never be able to push to one (#1367).
|
|
183
|
+
this.gistId = null;
|
|
184
|
+
this.lastFetchedEtag = null;
|
|
166
185
|
warn(MODULE, 'All Gist API paths failed, entering degraded mode', err);
|
|
167
186
|
// Try reading from local cache file
|
|
168
187
|
const cachePath = getStateCachePath();
|
|
@@ -222,6 +241,12 @@ export class GistStateStore {
|
|
|
222
241
|
return { gistId: localId, state, created: false, migrated: false };
|
|
223
242
|
}
|
|
224
243
|
catch (err) {
|
|
244
|
+
// See bootstrap(): corrupt/permission/rate-limit errors surface
|
|
245
|
+
// immediately rather than falling through (#1367).
|
|
246
|
+
if (err instanceof ConfigurationError)
|
|
247
|
+
throw err;
|
|
248
|
+
if (isRateLimitError(err))
|
|
249
|
+
throw err;
|
|
225
250
|
warn(MODULE, `bootstrapWithMigration: failed to fetch Gist ${localId}, will search/create`, err);
|
|
226
251
|
// Fall through to search
|
|
227
252
|
}
|
|
@@ -242,10 +267,16 @@ export class GistStateStore {
|
|
|
242
267
|
return { gistId: id, state, created: true, migrated: true };
|
|
243
268
|
}
|
|
244
269
|
catch (err) {
|
|
245
|
-
//
|
|
246
|
-
|
|
270
|
+
// Same surfacing contract as bootstrap() (#1367): configuration errors
|
|
271
|
+
// and rate limits rethrow; only genuine API unavailability degrades.
|
|
272
|
+
if (err instanceof ConfigurationError)
|
|
273
|
+
throw err;
|
|
274
|
+
if (isRateLimitError(err))
|
|
247
275
|
throw err;
|
|
248
|
-
// All API paths failed — enter degraded mode
|
|
276
|
+
// All API paths failed — enter degraded mode with the push path
|
|
277
|
+
// disarmed (see bootstrap()).
|
|
278
|
+
this.gistId = null;
|
|
279
|
+
this.lastFetchedEtag = null;
|
|
249
280
|
warn(MODULE, 'bootstrapWithMigration: all Gist API paths failed, entering degraded mode', err);
|
|
250
281
|
// Try reading from local cache file
|
|
251
282
|
const cachePath = getStateCachePath();
|
|
@@ -627,6 +658,10 @@ export class GistStateStore {
|
|
|
627
658
|
}
|
|
628
659
|
}
|
|
629
660
|
catch (err) {
|
|
661
|
+
// A rate-limited search must not read as "no Gist found" — bootstrap
|
|
662
|
+
// would proceed to create a duplicate Gist while throttled (#1367).
|
|
663
|
+
if (isRateLimitError(err))
|
|
664
|
+
throw err;
|
|
630
665
|
warn(MODULE, 'Failed to search Gists by description', err);
|
|
631
666
|
}
|
|
632
667
|
return null;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX,
|
|
|
8
8
|
export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
9
9
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
10
10
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
11
|
-
export { wrapUntrustedContent, extractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
|
|
11
|
+
export { wrapUntrustedContent, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
|
|
12
12
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
13
13
|
export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
|
|
14
14
|
export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
|
|
@@ -22,6 +22,8 @@ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsBy
|
|
|
22
22
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
23
23
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
24
24
|
export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
|
|
25
|
+
export { classifyIssueAvailability, fetchIssueVerification, type IssueAvailabilityVerdict, type IssueVerification, type LinkedPRLinkType, type VerifiedLinkedPR, type VerifyIssueParams, } from './issue-verification.js';
|
|
26
|
+
export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, type AttentionBucket, type AttentionInput, type AttentionSummary, } from './pr-attention.js';
|
|
25
27
|
export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type AntiLLMScanResult, } from './anti-llm-policy.js';
|
|
26
28
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
|
|
27
29
|
export { CONFIG_KEY_REGISTRY, type ConfigKeyDef, type SettableVia, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
package/dist/core/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks,
|
|
|
9
9
|
// Search/vetting now delegated to @oss-scout/core via commands/scout-bridge.ts
|
|
10
10
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
11
11
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
12
|
-
export { wrapUntrustedContent, extractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
|
|
12
|
+
export { wrapUntrustedContent, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
|
|
13
13
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
14
14
|
export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
|
|
15
15
|
export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
|
|
@@ -23,6 +23,8 @@ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsBy
|
|
|
23
23
|
export { computeContributionStats } from './stats.js';
|
|
24
24
|
export { fetchPRTemplate } from './pr-template.js';
|
|
25
25
|
export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from './linked-pr-classification.js';
|
|
26
|
+
export { classifyIssueAvailability, fetchIssueVerification, } from './issue-verification.js';
|
|
27
|
+
export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, } from './pr-attention.js';
|
|
26
28
|
export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
|
|
27
29
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
|
|
28
30
|
export { CONFIG_KEY_REGISTRY, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
|
@@ -12,7 +12,7 @@ import { getStateManager } from './state.js';
|
|
|
12
12
|
import { daysBetween } from './dates.js';
|
|
13
13
|
import { splitRepo, extractOwnerRepo, isOwnRepo } from './urls.js';
|
|
14
14
|
import { runWorkerPool, DEFAULT_CONCURRENCY } from './concurrency.js';
|
|
15
|
-
import { ConfigurationError, errorMessage } from './errors.js';
|
|
15
|
+
import { ConfigurationError, errorMessage, isRateLimitOrAuthError } from './errors.js';
|
|
16
16
|
import { debug, warn } from './logger.js';
|
|
17
17
|
const MODULE = 'issue-conversation';
|
|
18
18
|
const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
|
|
@@ -108,6 +108,14 @@ export class IssueConversationMonitor {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
catch (error) {
|
|
111
|
+
// Rate-limit / auth failures must propagate, not degrade to "fewer
|
|
112
|
+
// results" — under throttling every sibling analysis fails the same
|
|
113
|
+
// way and the partial result silently looks like a quiet day (#1391).
|
|
114
|
+
// runWorkerPool aborts remaining workers and rejects; daily.ts and
|
|
115
|
+
// dashboard-data.ts rethrow rate-limit/auth from their phase catches,
|
|
116
|
+
// aborting the run just like PRMonitor's 429s do.
|
|
117
|
+
if (isRateLimitOrAuthError(error))
|
|
118
|
+
throw error;
|
|
111
119
|
const msg = errorMessage(error);
|
|
112
120
|
failures.push({ issueUrl: item.html_url, error: msg });
|
|
113
121
|
warn(MODULE, `Error analyzing issue ${item.html_url}: ${msg}`);
|
|
@@ -117,7 +125,7 @@ export class IssueConversationMonitor {
|
|
|
117
125
|
warn(MODULE, `${failures.length}/${candidates.length} issue analysis call(s) failed`);
|
|
118
126
|
}
|
|
119
127
|
if (failures.length === candidates.length && candidates.length > 0) {
|
|
120
|
-
warn(MODULE, `All ${candidates.length} issue analysis call(s) failed. Possible systemic issue (
|
|
128
|
+
warn(MODULE, `All ${candidates.length} issue analysis call(s) failed. Possible systemic issue (network, GitHub availability).`);
|
|
121
129
|
}
|
|
122
130
|
// Sort: new_response first, then waiting, then acknowledged
|
|
123
131
|
const statusOrder = {
|
|
@@ -199,6 +207,11 @@ export class IssueConversationMonitor {
|
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
209
|
const labels = (item.labels || []).map((l) => l.name || '').filter(Boolean);
|
|
210
|
+
// Body excerpts stay RAW here on purpose (#1372): these objects feed the
|
|
211
|
+
// dashboard SPA and the CLI text renderers directly, and the matching
|
|
212
|
+
// above (acknowledgment / @mention) operates on raw text. The
|
|
213
|
+
// `<github-content>` fence is applied at the agent-facing serialization
|
|
214
|
+
// boundary in `toDailyOutput()` (commands/daily.ts).
|
|
202
215
|
const base = {
|
|
203
216
|
repo: repoFullName,
|
|
204
217
|
number: item.number,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic issue verification (#1353, #1354).
|
|
3
|
+
*
|
|
4
|
+
* One GraphQL round-trip answers the two questions the issue-scout agent
|
|
5
|
+
* historically hallucinated: "is this issue actually open?" and "is it
|
|
6
|
+
* actually taken?". The query fetches the issue's `state`/`stateReason`,
|
|
7
|
+
* its assignees, every cross-referenced PR with `closingIssuesReferences`,
|
|
8
|
+
* and `viewer.login` — so "is this my PR?" is derived from the same token
|
|
9
|
+
* that fetched the data instead of a cached username.
|
|
10
|
+
*
|
|
11
|
+
* Classification is a pure function (`classifyIssueAvailability`) so the
|
|
12
|
+
* verdict rules are unit-testable without network access. The distinction
|
|
13
|
+
* that matters (#1353): a PR whose `closingIssuesReferences` includes this
|
|
14
|
+
* issue is a real claim (`closing`); a PR that merely mentions the issue in
|
|
15
|
+
* its timeline is just a mention (`cross-referenced`) and must not drive a
|
|
16
|
+
* "Taken" verdict.
|
|
17
|
+
*/
|
|
18
|
+
import type { Octokit } from '@octokit/rest';
|
|
19
|
+
/** How a PR found in the issue's timeline relates to the issue. */
|
|
20
|
+
export type LinkedPRLinkType = 'closing' | 'cross-referenced';
|
|
21
|
+
export interface VerifiedLinkedPR {
|
|
22
|
+
number: number;
|
|
23
|
+
url: string;
|
|
24
|
+
title: string;
|
|
25
|
+
/** Lowercase normalization of GraphQL's OPEN/CLOSED/MERGED union. */
|
|
26
|
+
state: 'open' | 'closed' | 'merged';
|
|
27
|
+
isDraft: boolean;
|
|
28
|
+
/** PR author login; null for ghost (deleted) accounts. */
|
|
29
|
+
author: string | null;
|
|
30
|
+
/** True when the PR author is the authenticated user (case-insensitive). */
|
|
31
|
+
isOwn: boolean;
|
|
32
|
+
/** `closing` = the PR's closingIssuesReferences names this issue (a real
|
|
33
|
+
* claim). `cross-referenced` = timeline mention only (NOT a claim). */
|
|
34
|
+
linkType: LinkedPRLinkType;
|
|
35
|
+
updatedAt: string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The short-circuit signal consumers route on:
|
|
39
|
+
* - `closed` — issue is not open; stop all further analysis (#1353).
|
|
40
|
+
* - `own-open-pr` — the authenticated user already has an open closing PR;
|
|
41
|
+
* not a new opportunity (#1354).
|
|
42
|
+
* - `taken` — someone else has an open closing PR, or the issue is assigned
|
|
43
|
+
* to someone else.
|
|
44
|
+
* - `at-risk` — no hard claim, but signals competition or imminent closure
|
|
45
|
+
* (open cross-referencing PRs, or a merged closing PR awaiting issue close).
|
|
46
|
+
* - `available` — open, unassigned, no claiming PRs.
|
|
47
|
+
*/
|
|
48
|
+
export type IssueAvailabilityVerdict = 'closed' | 'own-open-pr' | 'taken' | 'at-risk' | 'available';
|
|
49
|
+
export interface IssueVerification {
|
|
50
|
+
url: string;
|
|
51
|
+
owner: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
number: number;
|
|
54
|
+
title: string;
|
|
55
|
+
/** Lowercase normalization of GraphQL's OPEN/CLOSED union. */
|
|
56
|
+
state: 'open' | 'closed';
|
|
57
|
+
/** Lowercase GraphQL IssueStateReason. `completed` = fixed upstream (mark
|
|
58
|
+
* Done); `not_planned` = wontfix (drop permanently). Note: an OPEN issue
|
|
59
|
+
* can carry `reopened` — never infer closed-ness from a non-null reason;
|
|
60
|
+
* branch on `state` only. */
|
|
61
|
+
stateReason: 'completed' | 'not_planned' | 'reopened' | 'duplicate' | null;
|
|
62
|
+
closedAt: string | null;
|
|
63
|
+
assignees: string[];
|
|
64
|
+
linkedPRs: VerifiedLinkedPR[];
|
|
65
|
+
verdict: IssueAvailabilityVerdict;
|
|
66
|
+
verdictReason: string;
|
|
67
|
+
/** Login of the authenticated user the verdict was computed for. */
|
|
68
|
+
userLogin: string;
|
|
69
|
+
}
|
|
70
|
+
/** Inputs for the pure verdict classifier — the fetched facts, no I/O. */
|
|
71
|
+
export interface ClassifyIssueAvailabilityInput {
|
|
72
|
+
state: 'open' | 'closed';
|
|
73
|
+
stateReason: IssueVerification['stateReason'];
|
|
74
|
+
assignees: string[];
|
|
75
|
+
linkedPRs: VerifiedLinkedPR[];
|
|
76
|
+
userLogin: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute the availability verdict from fetched facts. Rules in priority
|
|
80
|
+
* order; the first match wins.
|
|
81
|
+
*/
|
|
82
|
+
export declare function classifyIssueAvailability(input: ClassifyIssueAvailabilityInput): {
|
|
83
|
+
verdict: IssueAvailabilityVerdict;
|
|
84
|
+
verdictReason: string;
|
|
85
|
+
};
|
|
86
|
+
export interface VerifyIssueParams {
|
|
87
|
+
owner: string;
|
|
88
|
+
repo: string;
|
|
89
|
+
number: number;
|
|
90
|
+
}
|
|
91
|
+
export declare function fetchIssueVerification(octokit: Octokit, params: VerifyIssueParams): Promise<IssueVerification>;
|