@oss-autopilot/core 0.46.1 → 0.47.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 +46 -46
- package/dist/cli.bundle.cjs.map +3 -3
- package/dist/commands/dashboard-data.d.ts +8 -2
- package/dist/commands/dashboard-data.js +57 -6
- package/dist/commands/dashboard-server.js +10 -7
- package/dist/core/github-stats.d.ts +9 -1
- package/dist/core/github-stats.js +52 -0
- package/dist/core/state.d.ts +17 -1
- package/dist/core/state.js +45 -0
- package/dist/core/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
|
-
import { type DailyDigest, type AgentState, type CommentedIssue } from '../core/types.js';
|
|
6
|
+
import { type DailyDigest, type AgentState, type MergedPR, type StoredMergedPR, type CommentedIssue } from '../core/types.js';
|
|
7
7
|
export interface DashboardStats {
|
|
8
8
|
activePRs: number;
|
|
9
9
|
shelvedPRs: number;
|
|
@@ -11,7 +11,7 @@ export interface DashboardStats {
|
|
|
11
11
|
closedPRs: number;
|
|
12
12
|
mergeRate: string;
|
|
13
13
|
}
|
|
14
|
-
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState
|
|
14
|
+
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number): DashboardStats;
|
|
15
15
|
/**
|
|
16
16
|
* Merge fresh API counts into existing stored counts.
|
|
17
17
|
* Months present in the fresh data are updated; months only in the existing data are preserved.
|
|
@@ -31,6 +31,7 @@ export declare function updateMonthlyAnalytics(prs: Array<{
|
|
|
31
31
|
export interface DashboardFetchResult {
|
|
32
32
|
digest: DailyDigest;
|
|
33
33
|
commentedIssues: CommentedIssue[];
|
|
34
|
+
allMergedPRs: MergedPR[];
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Fetch fresh dashboard data from GitHub.
|
|
@@ -38,6 +39,11 @@ export interface DashboardFetchResult {
|
|
|
38
39
|
* Throws if the fetch fails entirely (caller should fall back to cached data).
|
|
39
40
|
*/
|
|
40
41
|
export declare function fetchDashboardData(token: string): Promise<DashboardFetchResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Convert StoredMergedPR[] to MergedPR[] by deriving repo and number from URL.
|
|
44
|
+
* Skips entries with unparseable URLs.
|
|
45
|
+
*/
|
|
46
|
+
export declare function storedToMergedPRs(stored: StoredMergedPR[]): MergedPR[];
|
|
41
47
|
/**
|
|
42
48
|
* Compute PRs grouped by repository from a digest and state.
|
|
43
49
|
* Used for chart data in the dashboard API.
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
|
-
import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
|
|
6
|
+
import { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit } from '../core/index.js';
|
|
7
7
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
|
-
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
9
|
+
import { emptyPRCountsResult, fetchMergedPRsSince } from '../core/github-stats.js';
|
|
10
|
+
import { parseGitHubUrl } from '../core/utils.js';
|
|
10
11
|
import { isBelowMinStars, } from '../core/types.js';
|
|
11
12
|
import { toShelvedPRRef, buildStarFilter } from './daily.js';
|
|
12
13
|
const MODULE = 'dashboard-data';
|
|
13
|
-
export function buildDashboardStats(digest, state) {
|
|
14
|
+
export function buildDashboardStats(digest, state, storedMergedCount) {
|
|
14
15
|
const summary = digest.summary || {
|
|
15
16
|
totalActivePRs: 0,
|
|
16
17
|
totalMergedAllTime: 0,
|
|
@@ -18,10 +19,15 @@ export function buildDashboardStats(digest, state) {
|
|
|
18
19
|
totalNeedingAttention: 0,
|
|
19
20
|
};
|
|
20
21
|
const minStars = state.config.minStars ?? 50;
|
|
22
|
+
// Use the higher of stored merged PR count and repoScores-derived count to avoid regressions
|
|
23
|
+
// when stored list hasn't caught up yet (first fetch caps at 300)
|
|
24
|
+
const mergedPRs = storedMergedCount !== undefined
|
|
25
|
+
? Math.max(storedMergedCount, summary.totalMergedAllTime)
|
|
26
|
+
: summary.totalMergedAllTime;
|
|
21
27
|
return {
|
|
22
28
|
activePRs: summary.totalActivePRs,
|
|
23
29
|
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
24
|
-
mergedPRs
|
|
30
|
+
mergedPRs,
|
|
25
31
|
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (isBelowMinStars(s.stargazersCount, minStars) ? 0 : s.closedWithoutMergeCount || 0), 0),
|
|
26
32
|
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
27
33
|
};
|
|
@@ -92,9 +98,13 @@ export async function fetchDashboardData(token) {
|
|
|
92
98
|
const stateManager = getStateManager();
|
|
93
99
|
const prMonitor = new PRMonitor(token);
|
|
94
100
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
101
|
+
const octokit = getOctokit(token);
|
|
102
|
+
const config = stateManager.getState().config;
|
|
95
103
|
// Build star filter from cached repoScores (#576)
|
|
96
104
|
const starFilter = buildStarFilter(stateManager.getState());
|
|
97
|
-
|
|
105
|
+
// Get watermark for incremental merged PR fetch
|
|
106
|
+
const watermark = stateManager.getMergedPRWatermark();
|
|
107
|
+
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues, newMergedPRs,] = await Promise.all([
|
|
98
108
|
prMonitor.fetchUserOpenPRs(),
|
|
99
109
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
100
110
|
if (isRateLimitOrAuthError(err))
|
|
@@ -135,6 +145,12 @@ export async function fetchDashboardData(token) {
|
|
|
135
145
|
failures: [{ issueUrl: 'N/A', error: `Issue conversation fetch failed: ${msg}` }],
|
|
136
146
|
};
|
|
137
147
|
}),
|
|
148
|
+
fetchMergedPRsSince(octokit, config, watermark).catch((err) => {
|
|
149
|
+
if (isRateLimitOrAuthError(err))
|
|
150
|
+
throw err;
|
|
151
|
+
warn(MODULE, `Failed to fetch merged PRs for storage: ${errorMessage(err)}`);
|
|
152
|
+
return [];
|
|
153
|
+
}),
|
|
138
154
|
]);
|
|
139
155
|
const commentedIssues = fetchedIssues.issues;
|
|
140
156
|
if (fetchedIssues.failures.length > 0) {
|
|
@@ -143,6 +159,15 @@ export async function fetchDashboardData(token) {
|
|
|
143
159
|
if (failures.length > 0) {
|
|
144
160
|
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
145
161
|
}
|
|
162
|
+
// Store new merged PRs incrementally (dedupes by URL)
|
|
163
|
+
try {
|
|
164
|
+
stateManager.addMergedPRs(newMergedPRs);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
warn(MODULE, `Failed to store merged PRs: ${errorMessage(error)}`);
|
|
168
|
+
}
|
|
169
|
+
// Convert stored merged PRs to full MergedPR type (derive repo/number from URL)
|
|
170
|
+
const allMergedPRs = storedToMergedPRs(stateManager.getMergedPRs());
|
|
146
171
|
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
147
172
|
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
148
173
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
@@ -163,7 +188,33 @@ export async function fetchDashboardData(token) {
|
|
|
163
188
|
warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
|
|
164
189
|
}
|
|
165
190
|
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
166
|
-
return { digest, commentedIssues };
|
|
191
|
+
return { digest, commentedIssues, allMergedPRs };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Convert StoredMergedPR[] to MergedPR[] by deriving repo and number from URL.
|
|
195
|
+
* Skips entries with unparseable URLs.
|
|
196
|
+
*/
|
|
197
|
+
export function storedToMergedPRs(stored) {
|
|
198
|
+
const results = [];
|
|
199
|
+
let skipped = 0;
|
|
200
|
+
for (const pr of stored) {
|
|
201
|
+
const parsed = parseGitHubUrl(pr.url);
|
|
202
|
+
if (!parsed) {
|
|
203
|
+
skipped++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
results.push({
|
|
207
|
+
url: pr.url,
|
|
208
|
+
repo: `${parsed.owner}/${parsed.repo}`,
|
|
209
|
+
number: parsed.number,
|
|
210
|
+
title: pr.title,
|
|
211
|
+
mergedAt: pr.mergedAt,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (skipped > 0) {
|
|
215
|
+
warn(MODULE, `Skipped ${skipped} stored merged PR(s) with unparseable URLs`);
|
|
216
|
+
}
|
|
217
|
+
return results;
|
|
167
218
|
}
|
|
168
219
|
/**
|
|
169
220
|
* Compute PRs grouped by repository from a digest and state.
|
|
@@ -12,7 +12,7 @@ import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js
|
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
13
|
import { warn } from '../core/logger.js';
|
|
14
14
|
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
|
|
15
|
-
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
|
|
15
|
+
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, } from './dashboard-data.js';
|
|
16
16
|
import { openInBrowser } from './startup.js';
|
|
17
17
|
import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
|
|
18
18
|
import { RateLimiter } from './rate-limiter.js';
|
|
@@ -70,11 +70,13 @@ function applyStatusOverrides(prs, state) {
|
|
|
70
70
|
/**
|
|
71
71
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
72
72
|
*/
|
|
73
|
-
function buildDashboardJson(digest, state, commentedIssues) {
|
|
73
|
+
function buildDashboardJson(digest, state, commentedIssues, allMergedPRs) {
|
|
74
74
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
75
75
|
const topRepos = computeTopRepos(prsByRepo);
|
|
76
76
|
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
77
|
-
|
|
77
|
+
// Derive allMergedPRs from state if not provided (e.g. initial load from cached state)
|
|
78
|
+
const mergedPRs = allMergedPRs ?? storedToMergedPRs(getStateManager().getMergedPRs());
|
|
79
|
+
const stats = buildDashboardStats(digest, state, mergedPRs.length);
|
|
78
80
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
79
81
|
return {
|
|
80
82
|
stats,
|
|
@@ -90,6 +92,7 @@ function buildDashboardJson(digest, state, commentedIssues) {
|
|
|
90
92
|
autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
|
|
91
93
|
commentedIssues,
|
|
92
94
|
issueResponses,
|
|
95
|
+
allMergedPRs: mergedPRs,
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
/**
|
|
@@ -139,7 +142,7 @@ function isValidOrigin(req, port) {
|
|
|
139
142
|
const origin = req.headers['origin'];
|
|
140
143
|
if (!origin)
|
|
141
144
|
return true; // No Origin header = same-origin request (non-browser or same-page)
|
|
142
|
-
const allowed = [`http://localhost:${port}`, `http://127.0.0.1:${port}`];
|
|
145
|
+
const allowed = [`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://oss.localhost:${port}`];
|
|
143
146
|
return allowed.includes(origin);
|
|
144
147
|
}
|
|
145
148
|
/**
|
|
@@ -329,7 +332,7 @@ export async function startDashboardServer(options) {
|
|
|
329
332
|
const result = await fetchDashboardData(currentToken);
|
|
330
333
|
cachedDigest = result.digest;
|
|
331
334
|
cachedCommentedIssues = result.commentedIssues;
|
|
332
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
335
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs);
|
|
333
336
|
sendJson(res, 200, cachedJsonData);
|
|
334
337
|
}
|
|
335
338
|
catch (error) {
|
|
@@ -433,7 +436,7 @@ export async function startDashboardServer(options) {
|
|
|
433
436
|
version: getCLIVersion(),
|
|
434
437
|
});
|
|
435
438
|
const serverUrl = `http://localhost:${actualPort}`;
|
|
436
|
-
warn(MODULE, `Dashboard server running at ${serverUrl}`);
|
|
439
|
+
warn(MODULE, `Dashboard server running at ${serverUrl} (also: http://oss.localhost:${actualPort})`);
|
|
437
440
|
// ── Background refresh ─────────────────────────────────────────────────
|
|
438
441
|
// Port is bound and PID file written — now fetch fresh data from GitHub
|
|
439
442
|
// so subsequent /api/data requests get live data instead of cached state.
|
|
@@ -442,7 +445,7 @@ export async function startDashboardServer(options) {
|
|
|
442
445
|
.then((result) => {
|
|
443
446
|
cachedDigest = result.digest;
|
|
444
447
|
cachedCommentedIssues = result.commentedIssues;
|
|
445
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
448
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs);
|
|
446
449
|
warn(MODULE, 'Background data refresh complete');
|
|
447
450
|
})
|
|
448
451
|
.catch((error) => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
|
-
import { ClosedPR, MergedPR, type StarFilter } from './types.js';
|
|
6
|
+
import { ClosedPR, MergedPR, StoredMergedPR, type StarFilter } from './types.js';
|
|
7
7
|
/** TTL for cached PR count results (24 hours — these stats change slowly). */
|
|
8
8
|
export declare const PR_COUNTS_CACHE_TTL_MS: number;
|
|
9
9
|
/** Return type shared by both merged and closed PR count functions. */
|
|
@@ -42,3 +42,11 @@ export declare function fetchRecentlyClosedPRs(octokit: Octokit, config: {
|
|
|
42
42
|
export declare function fetchRecentlyMergedPRs(octokit: Octokit, config: {
|
|
43
43
|
githubUsername: string;
|
|
44
44
|
}, days?: number): Promise<MergedPR[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Fetch merged PRs since a watermark date for incremental storage.
|
|
47
|
+
* If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
|
|
48
|
+
* Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
|
|
49
|
+
*/
|
|
50
|
+
export declare function fetchMergedPRsSince(octokit: Octokit, config: {
|
|
51
|
+
githubUsername: string;
|
|
52
|
+
}, since?: string): Promise<StoredMergedPR[]>;
|
|
@@ -235,3 +235,55 @@ 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) {
|
|
244
|
+
if (!config.githubUsername) {
|
|
245
|
+
warn(MODULE, 'Skipping merged PRs fetch: no githubUsername configured.');
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const dateFilter = since ? ` merged:>${since}` : '';
|
|
249
|
+
const q = `is:pr is:merged author:${config.githubUsername} -user:${config.githubUsername}${dateFilter}`;
|
|
250
|
+
debug(MODULE, `Fetching merged PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
251
|
+
const results = [];
|
|
252
|
+
let page = 1;
|
|
253
|
+
let fetched = 0;
|
|
254
|
+
while (true) {
|
|
255
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
256
|
+
q,
|
|
257
|
+
sort: 'updated',
|
|
258
|
+
order: 'desc',
|
|
259
|
+
per_page: 100,
|
|
260
|
+
page,
|
|
261
|
+
});
|
|
262
|
+
for (const item of data.items) {
|
|
263
|
+
const parsed = parseGitHubUrl(item.html_url);
|
|
264
|
+
if (!parsed) {
|
|
265
|
+
warn(MODULE, `Skipping merged PR with unparseable URL: ${item.html_url}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
269
|
+
continue;
|
|
270
|
+
const mergedAt = item.pull_request?.merged_at || item.closed_at || '';
|
|
271
|
+
if (!mergedAt) {
|
|
272
|
+
warn(MODULE, `Skipping merged PR with no merge date: ${item.html_url}`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
results.push({
|
|
276
|
+
url: item.html_url,
|
|
277
|
+
title: item.title,
|
|
278
|
+
mergedAt,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
fetched += data.items.length;
|
|
282
|
+
if (fetched >= data.total_count || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
page++;
|
|
286
|
+
}
|
|
287
|
+
debug(MODULE, `Fetched ${results.length} merged PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
288
|
+
return results;
|
|
289
|
+
}
|
package/dist/core/state.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* State management for the OSS Contribution Agent
|
|
3
3
|
* Persists state to a JSON file in ~/.oss-autopilot/
|
|
4
4
|
*/
|
|
5
|
-
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo, StatusOverride, FetchedPRStatus } from './types.js';
|
|
5
|
+
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo, StatusOverride, FetchedPRStatus, StoredMergedPR } from './types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Acquire an advisory file lock using exclusive-create (`wx` flag).
|
|
8
8
|
* If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
|
|
@@ -113,6 +113,22 @@ export declare class StateManager {
|
|
|
113
113
|
*/
|
|
114
114
|
setMonthlyOpenedCounts(counts: Record<string, number>): void;
|
|
115
115
|
setDailyActivityCounts(counts: Record<string, number>): void;
|
|
116
|
+
/**
|
|
117
|
+
* Get all stored merged PRs.
|
|
118
|
+
* @returns Array of stored merged PRs, sorted by mergedAt desc.
|
|
119
|
+
*/
|
|
120
|
+
getMergedPRs(): StoredMergedPR[];
|
|
121
|
+
/**
|
|
122
|
+
* Add new merged PRs to the stored list. Deduplicates by URL and sorts by mergedAt desc.
|
|
123
|
+
* @param prs - New merged PRs to add.
|
|
124
|
+
*/
|
|
125
|
+
addMergedPRs(prs: StoredMergedPR[]): void;
|
|
126
|
+
/**
|
|
127
|
+
* Get the most recent mergedAt timestamp from stored merged PRs.
|
|
128
|
+
* Used as the watermark for incremental fetching.
|
|
129
|
+
* @returns ISO date string of the most recent merge, or undefined if no stored PRs.
|
|
130
|
+
*/
|
|
131
|
+
getMergedPRWatermark(): string | undefined;
|
|
116
132
|
/**
|
|
117
133
|
* Store cached local repo scan results (#84).
|
|
118
134
|
* @param cache - The scan results, paths scanned, and timestamp.
|
package/dist/core/state.js
CHANGED
|
@@ -419,6 +419,10 @@ export class StateManager {
|
|
|
419
419
|
if (s.events === undefined) {
|
|
420
420
|
s.events = [];
|
|
421
421
|
}
|
|
422
|
+
// Migrate older states that don't have mergedPRs
|
|
423
|
+
if (s.mergedPRs === undefined) {
|
|
424
|
+
s.mergedPRs = [];
|
|
425
|
+
}
|
|
422
426
|
// Base requirements for all versions
|
|
423
427
|
const hasBaseFields = typeof s.version === 'number' &&
|
|
424
428
|
typeof s.repoScores === 'object' &&
|
|
@@ -536,6 +540,47 @@ export class StateManager {
|
|
|
536
540
|
setDailyActivityCounts(counts) {
|
|
537
541
|
this.state.dailyActivityCounts = counts;
|
|
538
542
|
}
|
|
543
|
+
// === Merged PR Storage ===
|
|
544
|
+
/**
|
|
545
|
+
* Get all stored merged PRs.
|
|
546
|
+
* @returns Array of stored merged PRs, sorted by mergedAt desc.
|
|
547
|
+
*/
|
|
548
|
+
getMergedPRs() {
|
|
549
|
+
return this.state.mergedPRs ?? [];
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Add new merged PRs to the stored list. Deduplicates by URL and sorts by mergedAt desc.
|
|
553
|
+
* @param prs - New merged PRs to add.
|
|
554
|
+
*/
|
|
555
|
+
addMergedPRs(prs) {
|
|
556
|
+
if (prs.length === 0)
|
|
557
|
+
return;
|
|
558
|
+
if (!this.state.mergedPRs) {
|
|
559
|
+
this.state.mergedPRs = [];
|
|
560
|
+
}
|
|
561
|
+
const existingUrls = new Set(this.state.mergedPRs.map((pr) => pr.url));
|
|
562
|
+
const newPRs = prs.filter((pr) => !existingUrls.has(pr.url));
|
|
563
|
+
if (newPRs.length === 0)
|
|
564
|
+
return;
|
|
565
|
+
this.state.mergedPRs.push(...newPRs);
|
|
566
|
+
this.state.mergedPRs.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt));
|
|
567
|
+
debug(MODULE, `Added ${newPRs.length} merged PRs (total: ${this.state.mergedPRs.length})`);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get the most recent mergedAt timestamp from stored merged PRs.
|
|
571
|
+
* Used as the watermark for incremental fetching.
|
|
572
|
+
* @returns ISO date string of the most recent merge, or undefined if no stored PRs.
|
|
573
|
+
*/
|
|
574
|
+
getMergedPRWatermark() {
|
|
575
|
+
const prs = this.state.mergedPRs;
|
|
576
|
+
if (!prs || prs.length === 0)
|
|
577
|
+
return undefined;
|
|
578
|
+
// List is sorted desc by mergedAt, so first element is most recent
|
|
579
|
+
const watermark = prs[0].mergedAt;
|
|
580
|
+
if (!watermark)
|
|
581
|
+
return undefined;
|
|
582
|
+
return watermark;
|
|
583
|
+
}
|
|
539
584
|
/**
|
|
540
585
|
* Store cached local repo scan results (#84).
|
|
541
586
|
* @param cache - The scan results, paths scanned, and timestamp.
|
package/dist/core/types.d.ts
CHANGED
|
@@ -307,6 +307,12 @@ export interface ClosedPR {
|
|
|
307
307
|
closedAt: string;
|
|
308
308
|
closedBy?: string;
|
|
309
309
|
}
|
|
310
|
+
/** Minimal merged PR data persisted in state.json. Repo/number derived from URL at display time. */
|
|
311
|
+
export interface StoredMergedPR {
|
|
312
|
+
url: string;
|
|
313
|
+
title: string;
|
|
314
|
+
mergedAt: string;
|
|
315
|
+
}
|
|
310
316
|
/** Minimal record of a PR that was merged, used in the daily digest. */
|
|
311
317
|
export interface MergedPR {
|
|
312
318
|
url: string;
|
|
@@ -383,6 +389,8 @@ export interface AgentState {
|
|
|
383
389
|
dailyActivityCounts?: Record<string, number>;
|
|
384
390
|
/** Cached local repo scan results (#84). Avoids re-scanning the filesystem every session. */
|
|
385
391
|
localRepoCache?: LocalRepoCache;
|
|
392
|
+
/** All merged PRs stored incrementally. Source of truth for the merged PR detail view. */
|
|
393
|
+
mergedPRs?: StoredMergedPR[];
|
|
386
394
|
activeIssues: TrackedIssue[];
|
|
387
395
|
}
|
|
388
396
|
/** Cached results from scanning the filesystem for local git clones (#84). */
|