@oss-autopilot/core 0.41.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/cli.bundle.cjs +17657 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +325 -0
- package/dist/commands/check-integration.d.ts +10 -0
- package/dist/commands/check-integration.js +192 -0
- package/dist/commands/comments.d.ts +24 -0
- package/dist/commands/comments.js +311 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +82 -0
- package/dist/commands/daily.d.ts +29 -0
- package/dist/commands/daily.js +433 -0
- package/dist/commands/dashboard-data.d.ts +45 -0
- package/dist/commands/dashboard-data.js +132 -0
- package/dist/commands/dashboard-templates.d.ts +23 -0
- package/dist/commands/dashboard-templates.js +1627 -0
- package/dist/commands/dashboard.d.ts +18 -0
- package/dist/commands/dashboard.js +134 -0
- package/dist/commands/dismiss.d.ts +13 -0
- package/dist/commands/dismiss.js +49 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +27 -0
- package/dist/commands/local-repos.d.ts +14 -0
- package/dist/commands/local-repos.js +155 -0
- package/dist/commands/parse-list.d.ts +13 -0
- package/dist/commands/parse-list.js +139 -0
- package/dist/commands/read.d.ts +12 -0
- package/dist/commands/read.js +33 -0
- package/dist/commands/search.d.ts +10 -0
- package/dist/commands/search.js +74 -0
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.js +276 -0
- package/dist/commands/shelve.d.ts +13 -0
- package/dist/commands/shelve.js +49 -0
- package/dist/commands/snooze.d.ts +18 -0
- package/dist/commands/snooze.js +83 -0
- package/dist/commands/startup.d.ts +33 -0
- package/dist/commands/startup.js +197 -0
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/track.d.ts +16 -0
- package/dist/commands/track.js +59 -0
- package/dist/commands/validation.d.ts +43 -0
- package/dist/commands/validation.js +112 -0
- package/dist/commands/vet.d.ts +10 -0
- package/dist/commands/vet.js +36 -0
- package/dist/core/checklist-analysis.d.ts +17 -0
- package/dist/core/checklist-analysis.js +39 -0
- package/dist/core/ci-analysis.d.ts +78 -0
- package/dist/core/ci-analysis.js +163 -0
- package/dist/core/comment-utils.d.ts +15 -0
- package/dist/core/comment-utils.js +52 -0
- package/dist/core/concurrency.d.ts +5 -0
- package/dist/core/concurrency.js +15 -0
- package/dist/core/daily-logic.d.ts +77 -0
- package/dist/core/daily-logic.js +512 -0
- package/dist/core/display-utils.d.ts +10 -0
- package/dist/core/display-utils.js +100 -0
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.js +34 -0
- package/dist/core/github-stats.d.ts +73 -0
- package/dist/core/github-stats.js +272 -0
- package/dist/core/github.d.ts +19 -0
- package/dist/core/github.js +60 -0
- package/dist/core/http-cache.d.ts +97 -0
- package/dist/core/http-cache.js +269 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +15 -0
- package/dist/core/issue-conversation.d.ts +29 -0
- package/dist/core/issue-conversation.js +231 -0
- package/dist/core/issue-discovery.d.ts +85 -0
- package/dist/core/issue-discovery.js +589 -0
- package/dist/core/issue-filtering.d.ts +51 -0
- package/dist/core/issue-filtering.js +103 -0
- package/dist/core/issue-scoring.d.ts +40 -0
- package/dist/core/issue-scoring.js +92 -0
- package/dist/core/issue-vetting.d.ts +49 -0
- package/dist/core/issue-vetting.js +536 -0
- package/dist/core/logger.d.ts +21 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/maintainer-analysis.d.ts +10 -0
- package/dist/core/maintainer-analysis.js +59 -0
- package/dist/core/pagination.d.ts +11 -0
- package/dist/core/pagination.js +20 -0
- package/dist/core/pr-monitor.d.ts +109 -0
- package/dist/core/pr-monitor.js +594 -0
- package/dist/core/review-analysis.d.ts +72 -0
- package/dist/core/review-analysis.js +163 -0
- package/dist/core/state.d.ts +371 -0
- package/dist/core/state.js +1089 -0
- package/dist/core/types.d.ts +507 -0
- package/dist/core/types.js +34 -0
- package/dist/core/utils.d.ts +249 -0
- package/dist/core/utils.js +422 -0
- package/dist/formatters/json.d.ts +269 -0
- package/dist/formatters/json.js +88 -0
- package/package.json +67 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Analysis - Review decision computation, unresponded comment detection,
|
|
3
|
+
* and self-reply filtering for PR reviews.
|
|
4
|
+
* Extracted from PRMonitor to isolate review-related logic (#263).
|
|
5
|
+
*/
|
|
6
|
+
import { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Determine review decision from reviews list.
|
|
9
|
+
* Groups reviews by user, keeping only the latest from each user,
|
|
10
|
+
* then checks for CHANGES_REQUESTED or APPROVED states.
|
|
11
|
+
*/
|
|
12
|
+
export function determineReviewDecision(reviews) {
|
|
13
|
+
if (reviews.length === 0) {
|
|
14
|
+
return 'review_required';
|
|
15
|
+
}
|
|
16
|
+
// Group reviews by user, keeping only the latest from each user
|
|
17
|
+
const latestByUser = new Map();
|
|
18
|
+
for (const review of reviews) {
|
|
19
|
+
const login = review.user?.login;
|
|
20
|
+
const state = review.state;
|
|
21
|
+
if (login && state) {
|
|
22
|
+
latestByUser.set(login, state);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const states = Array.from(latestByUser.values());
|
|
26
|
+
if (states.includes('CHANGES_REQUESTED')) {
|
|
27
|
+
return 'changes_requested';
|
|
28
|
+
}
|
|
29
|
+
if (states.includes('APPROVED')) {
|
|
30
|
+
return 'approved';
|
|
31
|
+
}
|
|
32
|
+
return 'review_required';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the date of the latest CHANGES_REQUESTED review (from any reviewer).
|
|
36
|
+
* Used to detect needs_changes status when review feedback is in inline comments.
|
|
37
|
+
*/
|
|
38
|
+
export function getLatestChangesRequestedDate(reviews) {
|
|
39
|
+
let latest;
|
|
40
|
+
for (const review of reviews) {
|
|
41
|
+
if (review.state === 'CHANGES_REQUESTED' && review.submitted_at) {
|
|
42
|
+
if (!latest || review.submitted_at > latest) {
|
|
43
|
+
latest = review.submitted_at;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return latest;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if all inline comments in a COMMENTED review are self-replies.
|
|
51
|
+
* A self-reply is when an author replies to their own earlier inline comment.
|
|
52
|
+
* Used to filter out informational follow-ups that don't require contributor action (#199).
|
|
53
|
+
*/
|
|
54
|
+
export function isAllSelfReplies(reviewId, reviewComments) {
|
|
55
|
+
const commentsForReview = reviewComments.filter((c) => c.pull_request_review_id === reviewId);
|
|
56
|
+
if (commentsForReview.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
// Build map of ALL comment IDs -> lowercase author for parent lookup
|
|
59
|
+
const authorMap = new Map();
|
|
60
|
+
for (const c of reviewComments) {
|
|
61
|
+
if (c.user?.login) {
|
|
62
|
+
authorMap.set(c.id, c.user.login.toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return commentsForReview.every((comment) => {
|
|
66
|
+
if (!comment.in_reply_to_id)
|
|
67
|
+
return false; // New thread, not a reply
|
|
68
|
+
const parentAuthor = authorMap.get(comment.in_reply_to_id);
|
|
69
|
+
const commentAuthor = comment.user?.login?.toLowerCase();
|
|
70
|
+
return parentAuthor != null && commentAuthor != null && parentAuthor === commentAuthor;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the body text of inline review comments for a COMMENTED review.
|
|
75
|
+
* Returns the first non-empty comment body, or undefined.
|
|
76
|
+
* Enables the acknowledgment filter to evaluate real content instead of
|
|
77
|
+
* synthetic placeholders (#199).
|
|
78
|
+
*/
|
|
79
|
+
export function getInlineCommentBody(reviewId, reviewComments) {
|
|
80
|
+
return reviewComments.find((c) => c.pull_request_review_id === reviewId && c.body?.trim())?.body?.trim();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if there are unresponded comments from maintainers.
|
|
84
|
+
* Combines issue comments and review comments into a timeline,
|
|
85
|
+
* then finds maintainer comments after the user's last comment.
|
|
86
|
+
*/
|
|
87
|
+
export function checkUnrespondedComments(comments, reviews, reviewComments, username) {
|
|
88
|
+
// Combine comments and reviews into a timeline
|
|
89
|
+
const timeline = [];
|
|
90
|
+
const usernameLower = username.toLowerCase();
|
|
91
|
+
for (const comment of comments) {
|
|
92
|
+
const author = comment.user?.login || 'unknown';
|
|
93
|
+
timeline.push({
|
|
94
|
+
author,
|
|
95
|
+
body: comment.body || '',
|
|
96
|
+
createdAt: comment.created_at,
|
|
97
|
+
isUser: author.toLowerCase() === usernameLower,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
for (const review of reviews) {
|
|
101
|
+
if (!review.submitted_at)
|
|
102
|
+
continue;
|
|
103
|
+
const body = (review.body || '').trim();
|
|
104
|
+
// Include COMMENTED reviews even without body text — they indicate
|
|
105
|
+
// inline review comments were posted and may need a response (#151).
|
|
106
|
+
// Skip other empty-body reviews (APPROVED, CHANGES_REQUESTED, DISMISSED)
|
|
107
|
+
// as those are state changes without comment text.
|
|
108
|
+
if (!body && review.state !== 'COMMENTED')
|
|
109
|
+
continue;
|
|
110
|
+
const author = review.user?.login || 'unknown';
|
|
111
|
+
// For inline-only COMMENTED reviews, skip pure self-replies (#199)
|
|
112
|
+
if (!body && review.state === 'COMMENTED' && review.id != null) {
|
|
113
|
+
if (isAllSelfReplies(review.id, reviewComments)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Resolve body: prefer actual text, then inline comment text, then synthetic placeholder
|
|
118
|
+
const resolvedBody = body ||
|
|
119
|
+
(review.id != null ? getInlineCommentBody(review.id, reviewComments) : undefined) ||
|
|
120
|
+
'(posted inline review comments)';
|
|
121
|
+
timeline.push({
|
|
122
|
+
author,
|
|
123
|
+
body: resolvedBody,
|
|
124
|
+
createdAt: review.submitted_at,
|
|
125
|
+
isUser: author.toLowerCase() === usernameLower,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Sort by date
|
|
129
|
+
timeline.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
130
|
+
// Find the last user comment
|
|
131
|
+
let lastUserCommentTime = null;
|
|
132
|
+
for (const item of timeline) {
|
|
133
|
+
if (item.isUser) {
|
|
134
|
+
lastUserCommentTime = new Date(item.createdAt);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Find maintainer comments after the user's last comment
|
|
138
|
+
let lastMaintainerComment;
|
|
139
|
+
for (const item of timeline) {
|
|
140
|
+
if (item.isUser)
|
|
141
|
+
continue; // Skip user's own comments
|
|
142
|
+
if (item.author === 'unknown')
|
|
143
|
+
continue; // Skip deleted/null accounts
|
|
144
|
+
if (isBotAuthor(item.author))
|
|
145
|
+
continue; // Skip bots
|
|
146
|
+
const itemTime = new Date(item.createdAt);
|
|
147
|
+
if (!lastUserCommentTime || itemTime > lastUserCommentTime) {
|
|
148
|
+
lastMaintainerComment = {
|
|
149
|
+
author: item.author,
|
|
150
|
+
body: item.body.slice(0, 200) + (item.body.length > 200 ? '...' : ''),
|
|
151
|
+
createdAt: item.createdAt,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Filter out pure acknowledgment comments that don't require a response
|
|
156
|
+
if (lastMaintainerComment && isAcknowledgmentComment(lastMaintainerComment.body)) {
|
|
157
|
+
lastMaintainerComment = undefined;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
hasUnrespondedComment: !!lastMaintainerComment,
|
|
161
|
+
lastMaintainerComment,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State management for the OSS Contribution Agent
|
|
3
|
+
* Persists state to a JSON file in ~/.oss-autopilot/
|
|
4
|
+
*/
|
|
5
|
+
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Acquire an advisory file lock using exclusive-create (`wx` flag).
|
|
8
|
+
* If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
|
|
9
|
+
* it is removed and re-acquired.
|
|
10
|
+
* @throws Error if the lock is held by another active process.
|
|
11
|
+
*/
|
|
12
|
+
export declare function acquireLock(lockPath: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Release an advisory file lock, but only if this process owns it.
|
|
15
|
+
* Silently ignores missing lock files or locks owned by other processes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function releaseLock(lockPath: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Write data to `filePath` atomically by first writing to a temporary file
|
|
20
|
+
* in the same directory and then renaming. Rename is atomic on POSIX filesystems,
|
|
21
|
+
* preventing partial/corrupt state files if the process crashes mid-write.
|
|
22
|
+
*/
|
|
23
|
+
export declare function atomicWriteFileSync(filePath: string, data: string, mode?: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
|
|
26
|
+
*
|
|
27
|
+
* Handles loading, saving, backup/restore, and v1-to-v2 migration of state. Supports
|
|
28
|
+
* an in-memory mode (no disk I/O) for use in tests. In v2 architecture, PR arrays are
|
|
29
|
+
* legacy -- open PRs are fetched fresh from GitHub on each run rather than stored locally.
|
|
30
|
+
*/
|
|
31
|
+
export declare class StateManager {
|
|
32
|
+
private state;
|
|
33
|
+
private readonly inMemoryOnly;
|
|
34
|
+
/**
|
|
35
|
+
* Create a new StateManager instance.
|
|
36
|
+
* @param inMemoryOnly - When true, state is held only in memory and never read from or
|
|
37
|
+
* written to disk. Useful for unit tests that need isolated state without side effects.
|
|
38
|
+
* Defaults to false (normal persistent mode).
|
|
39
|
+
*/
|
|
40
|
+
constructor(inMemoryOnly?: boolean);
|
|
41
|
+
/**
|
|
42
|
+
* Create a fresh state (v2: fresh GitHub fetching)
|
|
43
|
+
*/
|
|
44
|
+
private createFreshState;
|
|
45
|
+
/**
|
|
46
|
+
* Check if initial setup has been completed.
|
|
47
|
+
* @returns true if the user has run `/setup-oss` and completed configuration.
|
|
48
|
+
*/
|
|
49
|
+
isSetupComplete(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Mark setup as complete and record the completion timestamp.
|
|
52
|
+
*/
|
|
53
|
+
markSetupComplete(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Migrate state from legacy ./data/ location to ~/.oss-autopilot/
|
|
56
|
+
* Returns true if migration was performed
|
|
57
|
+
*/
|
|
58
|
+
private migrateFromLegacyLocation;
|
|
59
|
+
/**
|
|
60
|
+
* Load state from file, or create initial state if none exists.
|
|
61
|
+
* If the main state file is corrupted, attempts to restore from the most recent backup.
|
|
62
|
+
* Performs migration from legacy ./data/ location if needed.
|
|
63
|
+
*/
|
|
64
|
+
private load;
|
|
65
|
+
/**
|
|
66
|
+
* Attempt to restore state from the most recent valid backup.
|
|
67
|
+
* Returns the restored state if successful, or null if no valid backup is found.
|
|
68
|
+
*/
|
|
69
|
+
private tryRestoreFromBackup;
|
|
70
|
+
/**
|
|
71
|
+
* Validate that a loaded state has the required structure
|
|
72
|
+
* Handles both v1 (with PR arrays) and v2 (without)
|
|
73
|
+
*/
|
|
74
|
+
private isValidState;
|
|
75
|
+
/**
|
|
76
|
+
* Persist the current state to disk, creating a timestamped backup of the previous
|
|
77
|
+
* state file first. Updates `lastRunAt` to the current time. In in-memory mode,
|
|
78
|
+
* only updates `lastRunAt` without any file I/O. Retains at most 10 backup files.
|
|
79
|
+
*/
|
|
80
|
+
save(): void;
|
|
81
|
+
private cleanupBackups;
|
|
82
|
+
/**
|
|
83
|
+
* Get the current state as a read-only snapshot.
|
|
84
|
+
* @returns The full agent state. Callers should not mutate the returned object;
|
|
85
|
+
* use the StateManager methods to make changes.
|
|
86
|
+
*/
|
|
87
|
+
getState(): Readonly<AgentState>;
|
|
88
|
+
/**
|
|
89
|
+
* Store the latest daily digest for dashboard rendering.
|
|
90
|
+
* @param digest - The freshly generated digest from the current daily run.
|
|
91
|
+
*/
|
|
92
|
+
setLastDigest(digest: DailyDigest): void;
|
|
93
|
+
/**
|
|
94
|
+
* Store monthly merged PR counts for the contribution timeline chart.
|
|
95
|
+
* @param counts - Map of "YYYY-MM" strings to merged PR counts for that month.
|
|
96
|
+
*/
|
|
97
|
+
setMonthlyMergedCounts(counts: Record<string, number>): void;
|
|
98
|
+
/**
|
|
99
|
+
* Store monthly closed (without merge) PR counts for the contribution timeline and success rate charts.
|
|
100
|
+
* @param counts - Map of "YYYY-MM" strings to closed PR counts for that month.
|
|
101
|
+
*/
|
|
102
|
+
setMonthlyClosedCounts(counts: Record<string, number>): void;
|
|
103
|
+
/**
|
|
104
|
+
* Store monthly opened PR counts for the contribution timeline chart.
|
|
105
|
+
* @param counts - Map of "YYYY-MM" strings to opened PR counts for that month.
|
|
106
|
+
*/
|
|
107
|
+
setMonthlyOpenedCounts(counts: Record<string, number>): void;
|
|
108
|
+
setDailyActivityCounts(counts: Record<string, number>): void;
|
|
109
|
+
/**
|
|
110
|
+
* Store cached local repo scan results (#84).
|
|
111
|
+
* @param cache - The scan results, paths scanned, and timestamp.
|
|
112
|
+
*/
|
|
113
|
+
setLocalRepoCache(cache: LocalRepoCache): void;
|
|
114
|
+
/**
|
|
115
|
+
* Shallow-merge partial configuration updates into the current config.
|
|
116
|
+
* @param config - Partial config object whose properties override existing values.
|
|
117
|
+
*/
|
|
118
|
+
updateConfig(config: Partial<AgentState['config']>): void;
|
|
119
|
+
/**
|
|
120
|
+
* Append an event to the event log. Events are capped at {@link MAX_EVENTS} (1000);
|
|
121
|
+
* when the cap is exceeded, the oldest events are trimmed to stay within the limit.
|
|
122
|
+
* @param type - The event type (e.g. 'pr_tracked').
|
|
123
|
+
* @param data - Arbitrary key-value payload for the event.
|
|
124
|
+
*/
|
|
125
|
+
appendEvent(type: StateEventType, data: Record<string, unknown>): void;
|
|
126
|
+
/**
|
|
127
|
+
* Filter the event log to events of a specific type.
|
|
128
|
+
* @param type - The event type to filter by.
|
|
129
|
+
* @returns All events matching the given type, in chronological order.
|
|
130
|
+
*/
|
|
131
|
+
getEventsByType(type: StateEventType): StateEvent[];
|
|
132
|
+
/**
|
|
133
|
+
* Filter the event log to events within an inclusive time range.
|
|
134
|
+
* @param since - Start of the range (inclusive).
|
|
135
|
+
* @param until - End of the range (inclusive). Defaults to now.
|
|
136
|
+
* @returns Events whose timestamps fall within [since, until].
|
|
137
|
+
*/
|
|
138
|
+
getEventsInRange(since: Date, until?: Date): StateEvent[];
|
|
139
|
+
/**
|
|
140
|
+
* Add an issue to the active tracking list. If an issue with the same URL is
|
|
141
|
+
* already tracked, the call is a no-op.
|
|
142
|
+
* @param issue - The issue to begin tracking.
|
|
143
|
+
*/
|
|
144
|
+
addIssue(issue: TrackedIssue): void;
|
|
145
|
+
/**
|
|
146
|
+
* Add a repository to the trusted projects list. Trusted projects are prioritized
|
|
147
|
+
* in issue search results. No-op if the repo is already trusted.
|
|
148
|
+
* @param repo - Repository in "owner/repo" format.
|
|
149
|
+
*/
|
|
150
|
+
addTrustedProject(repo: string): void;
|
|
151
|
+
/**
|
|
152
|
+
* Test whether a repo matches any of the given exclusion lists.
|
|
153
|
+
* Both repo and org comparisons are case-insensitive (GitHub names are case-insensitive).
|
|
154
|
+
* @param repo - Repository in "owner/repo" format.
|
|
155
|
+
* @param repos - Full "owner/repo" strings (case-insensitive match).
|
|
156
|
+
* @param orgs - Org names (case-insensitive match against the owner segment of the repo).
|
|
157
|
+
*/
|
|
158
|
+
private static matchesExclusion;
|
|
159
|
+
/**
|
|
160
|
+
* Check whether a repository matches any exclusion rule from the current config.
|
|
161
|
+
* A repo is excluded if it matches an entry in `excludeRepos` (case-insensitive)
|
|
162
|
+
* or if its owner segment matches an entry in `excludeOrgs` (case-insensitive).
|
|
163
|
+
* @param repo - Repository in "owner/repo" format.
|
|
164
|
+
*/
|
|
165
|
+
private isExcluded;
|
|
166
|
+
/**
|
|
167
|
+
* Remove repositories matching the given exclusion lists from `trustedProjects`
|
|
168
|
+
* and `repoScores`. Called when a repo or org is newly excluded to keep stored
|
|
169
|
+
* data consistent with current filters.
|
|
170
|
+
* @param repos - Full "owner/repo" strings to exclude (case-insensitive match).
|
|
171
|
+
* @param orgs - Org names to exclude (case-insensitive match against owner segment).
|
|
172
|
+
*/
|
|
173
|
+
cleanupExcludedData(repos: string[], orgs: string[]): void;
|
|
174
|
+
/**
|
|
175
|
+
* Get the cached list of the user's GitHub starred repositories.
|
|
176
|
+
* @returns Array of "owner/repo" strings, or an empty array if never fetched.
|
|
177
|
+
*/
|
|
178
|
+
getStarredRepos(): string[];
|
|
179
|
+
/**
|
|
180
|
+
* Replace the cached starred repositories list and update the fetch timestamp.
|
|
181
|
+
* @param repos - Array of "owner/repo" strings from the user's GitHub stars.
|
|
182
|
+
*/
|
|
183
|
+
setStarredRepos(repos: string[]): void;
|
|
184
|
+
/**
|
|
185
|
+
* Check if the starred repos cache is stale (older than 24 hours) or has never been fetched.
|
|
186
|
+
* @returns true if the cache should be refreshed.
|
|
187
|
+
*/
|
|
188
|
+
isStarredReposStale(): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Shelve a PR by URL. Shelved PRs are excluded from capacity and actionable issues.
|
|
191
|
+
* They are auto-unshelved when a maintainer engages (needs_response, needs_changes, etc.).
|
|
192
|
+
* @param url - The full GitHub PR URL.
|
|
193
|
+
* @returns true if newly added, false if already shelved.
|
|
194
|
+
*/
|
|
195
|
+
shelvePR(url: string): boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Unshelve a PR by URL.
|
|
198
|
+
* @param url - The full GitHub PR URL.
|
|
199
|
+
* @returns true if found and removed, false if not shelved.
|
|
200
|
+
*/
|
|
201
|
+
unshelvePR(url: string): boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Check if a PR is shelved.
|
|
204
|
+
* @param url - The full GitHub PR URL.
|
|
205
|
+
* @returns true if the URL is in the shelved list.
|
|
206
|
+
*/
|
|
207
|
+
isPRShelved(url: string): boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Dismiss an issue by URL. Dismissed issues are excluded from `new_response` notifications
|
|
210
|
+
* until new activity occurs after the dismiss timestamp.
|
|
211
|
+
* @param url - The full GitHub issue URL.
|
|
212
|
+
* @param timestamp - ISO timestamp of when the issue was dismissed.
|
|
213
|
+
* @returns true if newly dismissed, false if already dismissed.
|
|
214
|
+
*/
|
|
215
|
+
dismissIssue(url: string, timestamp: string): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Undismiss an issue by URL.
|
|
218
|
+
* @param url - The full GitHub issue URL.
|
|
219
|
+
* @returns true if found and removed, false if not dismissed.
|
|
220
|
+
*/
|
|
221
|
+
undismissIssue(url: string): boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Get the timestamp when an issue was dismissed.
|
|
224
|
+
* @param url - The full GitHub issue URL.
|
|
225
|
+
* @returns The ISO dismiss timestamp, or undefined if not dismissed.
|
|
226
|
+
*/
|
|
227
|
+
getIssueDismissedAt(url: string): string | undefined;
|
|
228
|
+
/**
|
|
229
|
+
* Snooze a PR's CI failure for a given number of days.
|
|
230
|
+
* Snoozed PRs are excluded from actionable CI failure lists until the snooze expires.
|
|
231
|
+
* @param url - The full GitHub PR URL.
|
|
232
|
+
* @param reason - Why the CI failure is being snoozed (e.g., "upstream infrastructure issue").
|
|
233
|
+
* @param durationDays - Number of days to snooze. Default 7.
|
|
234
|
+
* @returns true if newly snoozed, false if already snoozed.
|
|
235
|
+
*/
|
|
236
|
+
snoozePR(url: string, reason: string, durationDays: number): boolean;
|
|
237
|
+
/**
|
|
238
|
+
* Unsnooze a PR by URL.
|
|
239
|
+
* @param url - The full GitHub PR URL.
|
|
240
|
+
* @returns true if found and removed, false if not snoozed.
|
|
241
|
+
*/
|
|
242
|
+
unsnoozePR(url: string): boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Check if a PR is currently snoozed (not expired).
|
|
245
|
+
* @param url - The full GitHub PR URL.
|
|
246
|
+
* @returns true if the PR is snoozed and the snooze has not expired.
|
|
247
|
+
*/
|
|
248
|
+
isSnoozed(url: string): boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Get snooze metadata for a PR.
|
|
251
|
+
* @param url - The full GitHub PR URL.
|
|
252
|
+
* @returns The snooze metadata, or undefined if not snoozed.
|
|
253
|
+
*/
|
|
254
|
+
getSnoozeInfo(url: string): SnoozeInfo | undefined;
|
|
255
|
+
/**
|
|
256
|
+
* Expire all snoozes that are past their `expiresAt` timestamp.
|
|
257
|
+
* @returns Array of PR URLs whose snoozes were expired.
|
|
258
|
+
*/
|
|
259
|
+
expireSnoozes(): string[];
|
|
260
|
+
/**
|
|
261
|
+
* Get the score record for a repository.
|
|
262
|
+
* @param repo - Repository in "owner/repo" format.
|
|
263
|
+
* @returns The RepoScore if the repo has been scored, or undefined if never evaluated.
|
|
264
|
+
*/
|
|
265
|
+
getRepoScore(repo: string): RepoScore | undefined;
|
|
266
|
+
/**
|
|
267
|
+
* Create a default repo score for a new repository
|
|
268
|
+
*/
|
|
269
|
+
private createDefaultRepoScore;
|
|
270
|
+
/**
|
|
271
|
+
* Calculate the score based on the repo's metrics.
|
|
272
|
+
* Base 5, logarithmic merge bonus (max +5), -1 per closed without merge (max -3),
|
|
273
|
+
* +1 if recently merged (within 90 days), +1 if responsive, -2 if hostile. Clamp 1-10.
|
|
274
|
+
*/
|
|
275
|
+
private calculateScore;
|
|
276
|
+
/**
|
|
277
|
+
* Update a repository's score with partial updates. If the repo has no existing score,
|
|
278
|
+
* a default score record is created first (base score 5). After applying updates, the
|
|
279
|
+
* numeric score is recalculated using the formula: base 5, logarithmic merge bonus (max +5),
|
|
280
|
+
* -1 per closed-without-merge (max -3), +1 if recently merged, +1 if responsive, -2 if hostile, clamped to [1, 10].
|
|
281
|
+
* @param repo - Repository in "owner/repo" format.
|
|
282
|
+
* @param updates - Updatable RepoScore fields to merge. The `score`, `repo`, and
|
|
283
|
+
* `lastEvaluatedAt` fields are not accepted — score is always derived via
|
|
284
|
+
* calculateScore(), and repo/lastEvaluatedAt are managed internally.
|
|
285
|
+
*/
|
|
286
|
+
updateRepoScore(repo: string, updates: RepoScoreUpdate): void;
|
|
287
|
+
/**
|
|
288
|
+
* Increment the merged PR count for a repository and recalculate its score.
|
|
289
|
+
* Routes through {@link updateRepoScore} for a single mutation path.
|
|
290
|
+
* @param repo - Repository in "owner/repo" format.
|
|
291
|
+
*/
|
|
292
|
+
incrementMergedCount(repo: string): void;
|
|
293
|
+
/**
|
|
294
|
+
* Increment the closed-without-merge count for a repository and recalculate its score.
|
|
295
|
+
* Routes through {@link updateRepoScore} for a single mutation path.
|
|
296
|
+
* @param repo - Repository in "owner/repo" format.
|
|
297
|
+
*/
|
|
298
|
+
incrementClosedCount(repo: string): void;
|
|
299
|
+
/**
|
|
300
|
+
* Mark a repository as having hostile maintainer comments and recalculate its score.
|
|
301
|
+
* This applies a -2 penalty to the score. Creates a default score record if needed.
|
|
302
|
+
* @param repo - Repository in "owner/repo" format.
|
|
303
|
+
*/
|
|
304
|
+
markRepoHostile(repo: string): void;
|
|
305
|
+
/**
|
|
306
|
+
* Get repositories where the user has at least one merged PR, sorted by merged count descending.
|
|
307
|
+
* These repos represent proven relationships with high merge probability.
|
|
308
|
+
* @returns Array of "owner/repo" strings for repos with mergedPRCount > 0.
|
|
309
|
+
*/
|
|
310
|
+
getReposWithMergedPRs(): string[];
|
|
311
|
+
/**
|
|
312
|
+
* Get repositories where the user has interacted (has a score record) but has NOT
|
|
313
|
+
* yet had a PR merged, excluding repos where the only interaction was rejection.
|
|
314
|
+
* These represent repos with open or in-progress PRs — relationships that benefit
|
|
315
|
+
* from continued search attention.
|
|
316
|
+
* @returns Array of "owner/repo" strings, sorted by score descending.
|
|
317
|
+
*/
|
|
318
|
+
getReposWithOpenPRs(): string[];
|
|
319
|
+
/**
|
|
320
|
+
* Get repositories with a score at or above the given threshold, sorted highest first.
|
|
321
|
+
* @param minScore - Minimum score (inclusive). Defaults to `config.minRepoScoreThreshold`.
|
|
322
|
+
* @returns Array of "owner/repo" strings for repos meeting the threshold.
|
|
323
|
+
*/
|
|
324
|
+
getHighScoringRepos(minScore?: number): string[];
|
|
325
|
+
/**
|
|
326
|
+
* Get repositories with a score at or below the given threshold, sorted lowest first.
|
|
327
|
+
* @param maxScore - Maximum score (inclusive). Defaults to `config.minRepoScoreThreshold`.
|
|
328
|
+
* @returns Array of "owner/repo" strings for repos at or below the threshold.
|
|
329
|
+
*/
|
|
330
|
+
getLowScoringRepos(maxScore?: number): string[];
|
|
331
|
+
/**
|
|
332
|
+
* Compute aggregate statistics from the current state. `mergedPRs` and `closedPRs` counts
|
|
333
|
+
* are summed from repo score records, excluding repos that match `excludeRepos` or `excludeOrgs`
|
|
334
|
+
* in the config (#211). `totalTracked` reflects the number of non-excluded repositories with
|
|
335
|
+
* score records.
|
|
336
|
+
* @returns A Stats snapshot computed from the current state.
|
|
337
|
+
*/
|
|
338
|
+
getStats(): Stats;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Aggregate statistics returned by {@link StateManager.getStats}.
|
|
342
|
+
*/
|
|
343
|
+
export interface Stats {
|
|
344
|
+
/** Total merged PRs across scored repositories (excludes repos/orgs in exclusion config). */
|
|
345
|
+
mergedPRs: number;
|
|
346
|
+
/** Total PRs closed without merge across scored repositories (excludes repos/orgs in exclusion config). */
|
|
347
|
+
closedPRs: number;
|
|
348
|
+
/** Number of active issues. Always 0 in v2 (sourced from fresh fetch instead). */
|
|
349
|
+
activeIssues: number;
|
|
350
|
+
/** Number of trusted projects (excludes repos/orgs in exclusion config). */
|
|
351
|
+
trustedProjects: number;
|
|
352
|
+
/** Merge success rate as a percentage string (e.g. "75.0%"). */
|
|
353
|
+
mergeRate: string;
|
|
354
|
+
/** Number of scored repositories (excludes repos/orgs in exclusion config). */
|
|
355
|
+
totalTracked: number;
|
|
356
|
+
/** Number of PRs needing a response. Always 0 in v2 (sourced from fresh fetch instead). */
|
|
357
|
+
needsResponse: number;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get the singleton StateManager instance, creating it on first call.
|
|
361
|
+
* Subsequent calls return the same instance. Use {@link resetStateManager} to
|
|
362
|
+
* clear the singleton (primarily for testing).
|
|
363
|
+
* @returns The shared StateManager instance.
|
|
364
|
+
*/
|
|
365
|
+
export declare function getStateManager(): StateManager;
|
|
366
|
+
/**
|
|
367
|
+
* Reset the singleton StateManager instance to null. The next call to
|
|
368
|
+
* {@link getStateManager} will create a fresh instance. Intended for test
|
|
369
|
+
* isolation -- should not be called in production code.
|
|
370
|
+
*/
|
|
371
|
+
export declare function resetStateManager(): void;
|