@oss-autopilot/core 0.51.1 → 0.52.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 +42 -42
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/core/repo-score-manager.d.ts +76 -0
- package/dist/core/repo-score-manager.js +204 -0
- package/dist/core/state-persistence.d.ts +53 -0
- package/dist/core/state-persistence.js +510 -0
- package/dist/core/state.d.ts +14 -311
- package/dist/core/state.js +48 -901
- package/package.json +1 -1
package/dist/core/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core module exports
|
|
3
3
|
* Re-exports all core functionality for convenient imports
|
|
4
4
|
*/
|
|
5
|
-
export { StateManager, getStateManager, resetStateManager } from './state.js';
|
|
5
|
+
export { StateManager, getStateManager, resetStateManager, type Stats } from './state.js';
|
|
6
6
|
export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
7
7
|
export { IssueDiscovery, type IssueCandidate, type SearchPriority, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } from './issue-discovery.js';
|
|
8
8
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository scoring logic for the OSS Contribution Agent.
|
|
3
|
+
* Functions that operate on AgentState for scoring, querying,
|
|
4
|
+
* and computing aggregate statistics. Mutation functions modify
|
|
5
|
+
* the passed state object in place; query functions are pure.
|
|
6
|
+
*/
|
|
7
|
+
import { AgentState, RepoScore, RepoScoreUpdate } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Calculate the score based on the repo's metrics.
|
|
10
|
+
* Base 5, logarithmic merge bonus (max +5), -1 per closed without merge (max -3),
|
|
11
|
+
* +1 if recently merged (within 90 days), +1 if responsive, -2 if hostile. Clamp 1-10.
|
|
12
|
+
*/
|
|
13
|
+
export declare function calculateScore(repoScore: RepoScore): number;
|
|
14
|
+
/**
|
|
15
|
+
* Get the score record for a repository.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getRepoScore(state: Readonly<AgentState>, repo: string): Readonly<RepoScore> | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Update a repository's score with partial updates. If the repo has no existing score,
|
|
20
|
+
* a default score record is created first (base score 5). After applying updates, the
|
|
21
|
+
* numeric score is recalculated.
|
|
22
|
+
*/
|
|
23
|
+
export declare function updateRepoScore(state: AgentState, repo: string, updates: RepoScoreUpdate): void;
|
|
24
|
+
/**
|
|
25
|
+
* Increment the merged PR count for a repository and recalculate its score.
|
|
26
|
+
*/
|
|
27
|
+
export declare function incrementMergedCount(state: AgentState, repo: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Increment the closed-without-merge count for a repository and recalculate its score.
|
|
30
|
+
*/
|
|
31
|
+
export declare function incrementClosedCount(state: AgentState, repo: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Mark a repository as having hostile maintainer comments and recalculate its score.
|
|
34
|
+
* This applies a -2 penalty to the score. Creates a default score record if needed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function markRepoHostile(state: AgentState, repo: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get repositories where the user has at least one merged PR, sorted by merged count descending.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getReposWithMergedPRs(state: Readonly<AgentState>): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Get repositories with a score record but no merge or closure outcomes yet
|
|
43
|
+
* (typically repos with only open PRs), sorted by score descending.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getReposWithOpenPRs(state: Readonly<AgentState>): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Get repositories with a score at or above the given threshold, sorted highest first.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getHighScoringRepos(state: Readonly<AgentState>, minScore?: number): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Get repositories with a score at or below the given threshold, sorted lowest first.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getLowScoringRepos(state: Readonly<AgentState>, maxScore?: number): string[];
|
|
54
|
+
/**
|
|
55
|
+
* Aggregate statistics returned by {@link getStats}.
|
|
56
|
+
*/
|
|
57
|
+
export interface Stats {
|
|
58
|
+
/** Total merged PRs across scored repositories (above minStars threshold). */
|
|
59
|
+
mergedPRs: number;
|
|
60
|
+
/** Total PRs closed without merge across scored repositories (above minStars threshold). */
|
|
61
|
+
closedPRs: number;
|
|
62
|
+
/** Number of active issues. Always 0 in v2 (sourced from fresh fetch instead). */
|
|
63
|
+
activeIssues: number;
|
|
64
|
+
/** Number of trusted projects. */
|
|
65
|
+
trustedProjects: number;
|
|
66
|
+
/** Merge success rate as a percentage string (e.g. "75.0%"). */
|
|
67
|
+
mergeRate: string;
|
|
68
|
+
/** Number of scored repositories (above minStars threshold). */
|
|
69
|
+
totalTracked: number;
|
|
70
|
+
/** Number of PRs needing a response. Always 0 in v2 (sourced from fresh fetch instead). */
|
|
71
|
+
needsResponse: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Compute aggregate statistics from the current state.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getStats(state: Readonly<AgentState>): Stats;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository scoring logic for the OSS Contribution Agent.
|
|
3
|
+
* Functions that operate on AgentState for scoring, querying,
|
|
4
|
+
* and computing aggregate statistics. Mutation functions modify
|
|
5
|
+
* the passed state object in place; query functions are pure.
|
|
6
|
+
*/
|
|
7
|
+
import { isBelowMinStars } from './types.js';
|
|
8
|
+
import { debug, warn } from './logger.js';
|
|
9
|
+
const MODULE = 'scoring';
|
|
10
|
+
/** Repo scores older than this are considered stale and excluded from low-scoring lists. */
|
|
11
|
+
const SCORE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
12
|
+
/**
|
|
13
|
+
* Create a default repo score for a new repository.
|
|
14
|
+
*/
|
|
15
|
+
function createDefaultRepoScore(repo) {
|
|
16
|
+
return {
|
|
17
|
+
repo,
|
|
18
|
+
score: 5, // Base score
|
|
19
|
+
mergedPRCount: 0,
|
|
20
|
+
closedWithoutMergeCount: 0,
|
|
21
|
+
avgResponseDays: null,
|
|
22
|
+
lastEvaluatedAt: new Date().toISOString(),
|
|
23
|
+
signals: {
|
|
24
|
+
hasActiveMaintainers: true, // Assume positive by default
|
|
25
|
+
isResponsive: false,
|
|
26
|
+
hasHostileComments: false,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Calculate the score based on the repo's metrics.
|
|
32
|
+
* Base 5, logarithmic merge bonus (max +5), -1 per closed without merge (max -3),
|
|
33
|
+
* +1 if recently merged (within 90 days), +1 if responsive, -2 if hostile. Clamp 1-10.
|
|
34
|
+
*/
|
|
35
|
+
export function calculateScore(repoScore) {
|
|
36
|
+
let score = 5; // Base score
|
|
37
|
+
// Logarithmic merge bonus (max +5): 1→+2, 2→+3, 3→+4, 4+→+5
|
|
38
|
+
if (repoScore.mergedPRCount > 0) {
|
|
39
|
+
const mergedBonus = Math.min(Math.round(Math.log2(repoScore.mergedPRCount + 1) * 2), 5);
|
|
40
|
+
score += mergedBonus;
|
|
41
|
+
}
|
|
42
|
+
// -1 per closed without merge (max -3)
|
|
43
|
+
const closedPenalty = Math.min(repoScore.closedWithoutMergeCount, 3);
|
|
44
|
+
score -= closedPenalty;
|
|
45
|
+
// +1 if lastMergedAt is set and within 90 days (recency)
|
|
46
|
+
if (repoScore.lastMergedAt) {
|
|
47
|
+
const lastMergedDate = new Date(repoScore.lastMergedAt);
|
|
48
|
+
if (isNaN(lastMergedDate.getTime())) {
|
|
49
|
+
warn(MODULE, `Invalid lastMergedAt date for ${repoScore.repo}: "${repoScore.lastMergedAt}". Skipping recency bonus.`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const msPerDay = 1000 * 60 * 60 * 24;
|
|
53
|
+
const daysSince = Math.floor((Date.now() - lastMergedDate.getTime()) / msPerDay);
|
|
54
|
+
if (daysSince <= 90) {
|
|
55
|
+
score += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// +1 if responsive
|
|
60
|
+
if (repoScore.signals.isResponsive) {
|
|
61
|
+
score += 1;
|
|
62
|
+
}
|
|
63
|
+
// -2 if hostile
|
|
64
|
+
if (repoScore.signals.hasHostileComments) {
|
|
65
|
+
score -= 2;
|
|
66
|
+
}
|
|
67
|
+
// Clamp to 1-10
|
|
68
|
+
return Math.max(1, Math.min(10, score));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get the score record for a repository.
|
|
72
|
+
*/
|
|
73
|
+
export function getRepoScore(state, repo) {
|
|
74
|
+
return state.repoScores[repo];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Update a repository's score with partial updates. If the repo has no existing score,
|
|
78
|
+
* a default score record is created first (base score 5). After applying updates, the
|
|
79
|
+
* numeric score is recalculated.
|
|
80
|
+
*/
|
|
81
|
+
export function updateRepoScore(state, repo, updates) {
|
|
82
|
+
if (!state.repoScores[repo]) {
|
|
83
|
+
state.repoScores[repo] = createDefaultRepoScore(repo);
|
|
84
|
+
}
|
|
85
|
+
const repoScore = state.repoScores[repo];
|
|
86
|
+
// Apply explicit field updates (skip undefined values to preserve existing data)
|
|
87
|
+
if (updates.mergedPRCount !== undefined)
|
|
88
|
+
repoScore.mergedPRCount = updates.mergedPRCount;
|
|
89
|
+
if (updates.closedWithoutMergeCount !== undefined)
|
|
90
|
+
repoScore.closedWithoutMergeCount = updates.closedWithoutMergeCount;
|
|
91
|
+
if (updates.avgResponseDays !== undefined)
|
|
92
|
+
repoScore.avgResponseDays = updates.avgResponseDays;
|
|
93
|
+
if (updates.lastMergedAt !== undefined)
|
|
94
|
+
repoScore.lastMergedAt = updates.lastMergedAt;
|
|
95
|
+
if (updates.stargazersCount !== undefined)
|
|
96
|
+
repoScore.stargazersCount = updates.stargazersCount;
|
|
97
|
+
if (updates.signals) {
|
|
98
|
+
repoScore.signals = { ...repoScore.signals, ...updates.signals };
|
|
99
|
+
}
|
|
100
|
+
// Recalculate score
|
|
101
|
+
repoScore.score = calculateScore(repoScore);
|
|
102
|
+
repoScore.lastEvaluatedAt = new Date().toISOString();
|
|
103
|
+
debug(MODULE, `Updated repo score for ${repo}: ${repoScore.score}/10`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Increment the merged PR count for a repository and recalculate its score.
|
|
107
|
+
*/
|
|
108
|
+
export function incrementMergedCount(state, repo) {
|
|
109
|
+
const newCount = (state.repoScores[repo]?.mergedPRCount ?? 0) + 1;
|
|
110
|
+
updateRepoScore(state, repo, {
|
|
111
|
+
mergedPRCount: newCount,
|
|
112
|
+
lastMergedAt: new Date().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Increment the closed-without-merge count for a repository and recalculate its score.
|
|
117
|
+
*/
|
|
118
|
+
export function incrementClosedCount(state, repo) {
|
|
119
|
+
const newCount = (state.repoScores[repo]?.closedWithoutMergeCount ?? 0) + 1;
|
|
120
|
+
updateRepoScore(state, repo, { closedWithoutMergeCount: newCount });
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Mark a repository as having hostile maintainer comments and recalculate its score.
|
|
124
|
+
* This applies a -2 penalty to the score. Creates a default score record if needed.
|
|
125
|
+
*/
|
|
126
|
+
export function markRepoHostile(state, repo) {
|
|
127
|
+
updateRepoScore(state, repo, { signals: { hasHostileComments: true } });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get repositories where the user has at least one merged PR, sorted by merged count descending.
|
|
131
|
+
*/
|
|
132
|
+
export function getReposWithMergedPRs(state) {
|
|
133
|
+
return Object.values(state.repoScores)
|
|
134
|
+
.filter((rs) => rs.mergedPRCount > 0)
|
|
135
|
+
.sort((a, b) => b.mergedPRCount - a.mergedPRCount)
|
|
136
|
+
.map((rs) => rs.repo);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get repositories with a score record but no merge or closure outcomes yet
|
|
140
|
+
* (typically repos with only open PRs), sorted by score descending.
|
|
141
|
+
*/
|
|
142
|
+
export function getReposWithOpenPRs(state) {
|
|
143
|
+
return Object.values(state.repoScores)
|
|
144
|
+
.filter((rs) => rs.mergedPRCount === 0 && rs.closedWithoutMergeCount === 0)
|
|
145
|
+
.sort((a, b) => b.score - a.score)
|
|
146
|
+
.map((rs) => rs.repo);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get repositories with a score at or above the given threshold, sorted highest first.
|
|
150
|
+
*/
|
|
151
|
+
export function getHighScoringRepos(state, minScore) {
|
|
152
|
+
const threshold = minScore ?? state.config.minRepoScoreThreshold;
|
|
153
|
+
return Object.values(state.repoScores)
|
|
154
|
+
.filter((rs) => rs.score >= threshold)
|
|
155
|
+
.sort((a, b) => b.score - a.score)
|
|
156
|
+
.map((rs) => rs.repo);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get repositories with a score at or below the given threshold, sorted lowest first.
|
|
160
|
+
*/
|
|
161
|
+
export function getLowScoringRepos(state, maxScore) {
|
|
162
|
+
const threshold = maxScore ?? state.config.minRepoScoreThreshold;
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
return Object.values(state.repoScores)
|
|
165
|
+
.filter((rs) => {
|
|
166
|
+
if (rs.score > threshold)
|
|
167
|
+
return false;
|
|
168
|
+
// Stale scores (>30 days) should not permanently block repos (#487)
|
|
169
|
+
const age = now - new Date(rs.lastEvaluatedAt).getTime();
|
|
170
|
+
if (!Number.isFinite(age)) {
|
|
171
|
+
warn(MODULE, `Invalid lastEvaluatedAt for repo ${rs.repo}: "${rs.lastEvaluatedAt}", treating as stale`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return age <= SCORE_TTL_MS;
|
|
175
|
+
})
|
|
176
|
+
.sort((a, b) => a.score - b.score)
|
|
177
|
+
.map((rs) => rs.repo);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Compute aggregate statistics from the current state.
|
|
181
|
+
*/
|
|
182
|
+
export function getStats(state) {
|
|
183
|
+
let totalMerged = 0;
|
|
184
|
+
let totalClosed = 0;
|
|
185
|
+
let totalTracked = 0;
|
|
186
|
+
for (const score of Object.values(state.repoScores)) {
|
|
187
|
+
if (isBelowMinStars(score.stargazersCount, state.config.minStars ?? 50))
|
|
188
|
+
continue;
|
|
189
|
+
totalTracked++;
|
|
190
|
+
totalMerged += score.mergedPRCount;
|
|
191
|
+
totalClosed += score.closedWithoutMergeCount;
|
|
192
|
+
}
|
|
193
|
+
const completed = totalMerged + totalClosed;
|
|
194
|
+
const mergeRate = completed > 0 ? (totalMerged / completed) * 100 : 0;
|
|
195
|
+
return {
|
|
196
|
+
mergedPRs: totalMerged,
|
|
197
|
+
closedPRs: totalClosed,
|
|
198
|
+
activeIssues: 0,
|
|
199
|
+
trustedProjects: state.config.trustedProjects.length,
|
|
200
|
+
mergeRate: mergeRate.toFixed(1) + '%',
|
|
201
|
+
totalTracked,
|
|
202
|
+
needsResponse: 0,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence layer for the OSS Contribution Agent.
|
|
3
|
+
* Handles file I/O, locking, backup/restore, and v1-to-v2 migration.
|
|
4
|
+
* No module-level mutable state — functions accept/return AgentState objects.
|
|
5
|
+
*/
|
|
6
|
+
import { AgentState } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Acquire an advisory file lock using exclusive-create (`wx` flag).
|
|
9
|
+
* If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
|
|
10
|
+
* it is removed and re-acquired.
|
|
11
|
+
* @throws Error if the lock is held by another active process.
|
|
12
|
+
*/
|
|
13
|
+
export declare function acquireLock(lockPath: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Release an advisory file lock, but only if this process owns it.
|
|
16
|
+
* Silently ignores missing lock files or locks owned by other processes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function releaseLock(lockPath: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Write data to `filePath` atomically by first writing to a temporary file
|
|
21
|
+
* in the same directory and then renaming. Rename is atomic on POSIX filesystems,
|
|
22
|
+
* preventing partial/corrupt state files if the process crashes mid-write.
|
|
23
|
+
*/
|
|
24
|
+
export declare function atomicWriteFileSync(filePath: string, data: string, mode?: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Create a fresh state (v2: fresh GitHub fetching).
|
|
27
|
+
*/
|
|
28
|
+
export declare function createFreshState(): AgentState;
|
|
29
|
+
/**
|
|
30
|
+
* Load state from file, or create initial state if none exists.
|
|
31
|
+
* If the main state file is corrupted, attempts to restore from the most recent backup.
|
|
32
|
+
* Performs migration from legacy ./data/ location if needed.
|
|
33
|
+
* @returns Object with the loaded state and the file's mtime (for change detection).
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadState(): {
|
|
36
|
+
state: AgentState;
|
|
37
|
+
mtimeMs: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Persist state to disk, creating a timestamped backup of the previous
|
|
41
|
+
* state file first. Retains at most 10 backup files.
|
|
42
|
+
* @returns The file's mtime after writing (for change detection).
|
|
43
|
+
*/
|
|
44
|
+
export declare function saveState(state: Readonly<AgentState>): number;
|
|
45
|
+
/**
|
|
46
|
+
* Re-read state from disk if the file has been modified since the last load/save.
|
|
47
|
+
* Uses mtime comparison (single statSync call) to avoid unnecessary JSON parsing.
|
|
48
|
+
* @returns The new state and mtime if reloaded, or null if no change detected.
|
|
49
|
+
*/
|
|
50
|
+
export declare function reloadStateIfChanged(lastLoadedMtimeMs: number): {
|
|
51
|
+
state: AgentState;
|
|
52
|
+
mtimeMs: number;
|
|
53
|
+
} | null;
|