@oss-autopilot/core 0.53.1 → 0.55.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 +63 -63
- package/dist/commands/comments.js +0 -1
- package/dist/commands/config.js +45 -5
- package/dist/commands/daily.js +197 -162
- package/dist/commands/dashboard-data.js +37 -30
- package/dist/commands/dashboard-server.js +8 -1
- package/dist/commands/dismiss.js +0 -6
- package/dist/commands/init.js +0 -1
- package/dist/commands/local-repos.js +1 -2
- package/dist/commands/move.js +12 -11
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.js +166 -130
- package/dist/commands/shelve.js +10 -10
- package/dist/commands/startup.js +30 -14
- package/dist/core/ci-analysis.d.ts +6 -0
- package/dist/core/ci-analysis.js +91 -12
- package/dist/core/daily-logic.js +24 -33
- package/dist/core/display-utils.js +22 -2
- package/dist/core/github-stats.d.ts +1 -1
- package/dist/core/github-stats.js +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/issue-discovery.d.ts +7 -44
- package/dist/core/issue-discovery.js +83 -188
- package/dist/core/issue-eligibility.d.ts +35 -0
- package/dist/core/issue-eligibility.js +126 -0
- package/dist/core/issue-vetting.d.ts +6 -21
- package/dist/core/issue-vetting.js +15 -279
- package/dist/core/pr-monitor.d.ts +14 -16
- package/dist/core/pr-monitor.js +26 -90
- package/dist/core/repo-health.d.ts +24 -0
- package/dist/core/repo-health.js +193 -0
- package/dist/core/repo-score-manager.js +2 -0
- package/dist/core/search-phases.d.ts +55 -0
- package/dist/core/search-phases.js +155 -0
- package/dist/core/state.d.ts +11 -0
- package/dist/core/state.js +63 -4
- package/dist/core/status-determination.d.ts +2 -0
- package/dist/core/status-determination.js +82 -22
- package/dist/core/types.d.ts +23 -2
- package/dist/core/types.js +7 -0
- package/dist/formatters/json.d.ts +1 -1
- package/package.json +1 -1
|
@@ -157,7 +157,6 @@ export async function runClaim(options) {
|
|
|
157
157
|
updatedAt: new Date().toISOString(),
|
|
158
158
|
vetted: false,
|
|
159
159
|
});
|
|
160
|
-
stateManager.save();
|
|
161
160
|
}
|
|
162
161
|
catch (error) {
|
|
163
162
|
console.error(`Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`);
|
package/dist/commands/config.js
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
* Shows or updates configuration
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager } from '../core/index.js';
|
|
6
|
+
import { ISSUE_SCOPES } from '../core/types.js';
|
|
6
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
|
+
function validateScope(value) {
|
|
9
|
+
if (!ISSUE_SCOPES.includes(value)) {
|
|
10
|
+
throw new Error(`Invalid scope "${value}". Valid scopes: ${ISSUE_SCOPES.join(', ')}`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
7
14
|
export async function runConfig(options) {
|
|
8
15
|
const stateManager = getStateManager();
|
|
9
16
|
const currentConfig = stateManager.getState().config;
|
|
@@ -30,6 +37,33 @@ export async function runConfig(options) {
|
|
|
30
37
|
stateManager.updateConfig({ labels: [...currentConfig.labels, value] });
|
|
31
38
|
}
|
|
32
39
|
break;
|
|
40
|
+
case 'remove-label':
|
|
41
|
+
if (!currentConfig.labels.includes(value)) {
|
|
42
|
+
throw new Error(`Label "${value}" is not currently configured. Current labels: ${currentConfig.labels.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
stateManager.updateConfig({ labels: currentConfig.labels.filter((l) => l !== value) });
|
|
45
|
+
break;
|
|
46
|
+
case 'add-scope': {
|
|
47
|
+
const scope = validateScope(value);
|
|
48
|
+
const currentScopes = currentConfig.scope ?? [];
|
|
49
|
+
if (!currentScopes.includes(scope)) {
|
|
50
|
+
stateManager.updateConfig({ scope: [...currentScopes, scope] });
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'remove-scope': {
|
|
55
|
+
const scope = validateScope(value);
|
|
56
|
+
const existingScopes = currentConfig.scope ?? [];
|
|
57
|
+
if (!existingScopes.includes(scope)) {
|
|
58
|
+
throw new Error(`Scope "${value}" is not currently set`);
|
|
59
|
+
}
|
|
60
|
+
const filtered = existingScopes.filter((s) => s !== scope);
|
|
61
|
+
if (filtered.length === 0) {
|
|
62
|
+
throw new Error('Cannot remove the last scope. Use setup to clear scopes entirely.');
|
|
63
|
+
}
|
|
64
|
+
stateManager.updateConfig({ scope: filtered });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
33
67
|
case 'exclude-repo': {
|
|
34
68
|
const parts = value.split('/');
|
|
35
69
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -37,8 +71,10 @@ export async function runConfig(options) {
|
|
|
37
71
|
}
|
|
38
72
|
const valueLower = value.toLowerCase();
|
|
39
73
|
if (!currentConfig.excludeRepos.some((r) => r.toLowerCase() === valueLower)) {
|
|
40
|
-
stateManager.
|
|
41
|
-
|
|
74
|
+
stateManager.batch(() => {
|
|
75
|
+
stateManager.updateConfig({ excludeRepos: [...currentConfig.excludeRepos, value] });
|
|
76
|
+
stateManager.cleanupExcludedData([value], []);
|
|
77
|
+
});
|
|
42
78
|
}
|
|
43
79
|
break;
|
|
44
80
|
}
|
|
@@ -48,14 +84,18 @@ export async function runConfig(options) {
|
|
|
48
84
|
}
|
|
49
85
|
const currentOrgs = currentConfig.excludeOrgs ?? [];
|
|
50
86
|
if (!currentOrgs.some((o) => o.toLowerCase() === value.toLowerCase())) {
|
|
51
|
-
stateManager.
|
|
52
|
-
|
|
87
|
+
stateManager.batch(() => {
|
|
88
|
+
stateManager.updateConfig({ excludeOrgs: [...currentOrgs, value] });
|
|
89
|
+
stateManager.cleanupExcludedData([], [value]);
|
|
90
|
+
});
|
|
53
91
|
}
|
|
54
92
|
break;
|
|
55
93
|
}
|
|
94
|
+
case 'issueListPath':
|
|
95
|
+
stateManager.updateConfig({ issueListPath: value || undefined });
|
|
96
|
+
break;
|
|
56
97
|
default:
|
|
57
98
|
throw new Error(`Unknown config key: ${options.key}`);
|
|
58
99
|
}
|
|
59
|
-
stateManager.save();
|
|
60
100
|
return { success: true, key: options.key, value };
|
|
61
101
|
}
|
package/dist/commands/daily.js
CHANGED
|
@@ -123,110 +123,124 @@ async function fetchPRData(prMonitor, token) {
|
|
|
123
123
|
*/
|
|
124
124
|
async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
125
125
|
const stateManager = getStateManager();
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
126
|
+
// Batch all synchronous score mutations for a single disk write.
|
|
127
|
+
// Per-repo try-catch: a single corrupted repo should not prevent updates to others.
|
|
128
|
+
// Outer try-catch: save failure should not crash the daily check (in-memory mutations still apply).
|
|
129
|
+
try {
|
|
130
|
+
stateManager.batch(() => {
|
|
131
|
+
// Reset stale repos first (so excluded/removed repos get zeroed).
|
|
132
|
+
// Guard: if the API returned zero results but we have existing repos with merged PRs,
|
|
133
|
+
// skip the reset to avoid wiping scores due to transient API failures.
|
|
134
|
+
const existingReposWithMerges = Object.values(stateManager.getState().repoScores).filter((s) => s.mergedPRCount > 0);
|
|
135
|
+
if (mergedCounts.size === 0 && existingReposWithMerges.length > 0) {
|
|
136
|
+
warn(MODULE, `Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
138
|
+
else {
|
|
139
|
+
for (const score of Object.values(stateManager.getState().repoScores)) {
|
|
140
|
+
if (!mergedCounts.has(score.repo)) {
|
|
141
|
+
stateManager.updateRepoScore(score.repo, { mergedPRCount: 0 });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Update merged/closed counts
|
|
146
|
+
let mergedCountFailures = 0;
|
|
147
|
+
for (const [repo, { count, lastMergedAt }] of mergedCounts) {
|
|
148
|
+
try {
|
|
149
|
+
stateManager.updateRepoScore(repo, { mergedPRCount: count, lastMergedAt: lastMergedAt || undefined });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
mergedCountFailures++;
|
|
153
|
+
warn(MODULE, `Failed to update merged count for ${repo}: ${errorMessage(error)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
157
|
+
warn(MODULE, `[ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
|
|
158
|
+
}
|
|
159
|
+
// Populate closedWithoutMergeCount in repo scores.
|
|
160
|
+
const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
|
|
161
|
+
if (closedCounts.size === 0 && existingReposWithClosed.length > 0) {
|
|
162
|
+
warn(MODULE, `API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
|
|
163
|
+
}
|
|
164
|
+
let closedCountFailures = 0;
|
|
165
|
+
for (const [repo, count] of closedCounts) {
|
|
166
|
+
try {
|
|
167
|
+
stateManager.updateRepoScore(repo, { closedWithoutMergeCount: count });
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
closedCountFailures++;
|
|
171
|
+
warn(MODULE, `Failed to update closed count for ${repo}: ${errorMessage(error)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
|
|
175
|
+
warn(MODULE, `[ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
|
|
176
|
+
}
|
|
177
|
+
// Update repo signals from observed open PR data
|
|
178
|
+
const repoSignals = computeRepoSignals(prs);
|
|
179
|
+
let signalUpdateFailures = 0;
|
|
180
|
+
for (const [repo, signals] of repoSignals) {
|
|
181
|
+
try {
|
|
182
|
+
stateManager.updateRepoScore(repo, { signals });
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
signalUpdateFailures++;
|
|
186
|
+
warn(MODULE, `Failed to update signals for ${repo}: ${errorMessage(error)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
|
|
190
|
+
warn(MODULE, `[ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
189
193
|
}
|
|
190
|
-
|
|
191
|
-
warn(MODULE, `
|
|
194
|
+
catch (error) {
|
|
195
|
+
warn(MODULE, `Failed to persist repo score updates: ${errorMessage(error)}`);
|
|
192
196
|
}
|
|
193
|
-
// Fetch
|
|
197
|
+
// Fetch metadata (stars + language) for all scored repos — async, so outside the batch above
|
|
194
198
|
const allRepos = Object.keys(stateManager.getState().repoScores);
|
|
195
|
-
let
|
|
199
|
+
let repoMetadata;
|
|
196
200
|
try {
|
|
197
|
-
|
|
201
|
+
repoMetadata = await prMonitor.fetchRepoMetadata(allRepos);
|
|
198
202
|
}
|
|
199
203
|
catch (error) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
if (isRateLimitOrAuthError(error))
|
|
205
|
+
throw error;
|
|
206
|
+
warn(MODULE, `Failed to fetch repo metadata: ${errorMessage(error)}`);
|
|
207
|
+
warn(MODULE, 'Repos without cached metadata will be excluded from dashboard stats and metadata badges until fetched on the next successful run.');
|
|
208
|
+
repoMetadata = new Map();
|
|
203
209
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
210
|
+
// Batch metadata + trust sync mutations for a single disk write
|
|
211
|
+
try {
|
|
212
|
+
stateManager.batch(() => {
|
|
213
|
+
let metadataUpdateFailures = 0;
|
|
214
|
+
for (const [repo, { stars, language }] of repoMetadata) {
|
|
215
|
+
try {
|
|
216
|
+
stateManager.updateRepoScore(repo, { stargazersCount: stars, language });
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
metadataUpdateFailures++;
|
|
220
|
+
warn(MODULE, `Failed to update metadata for ${repo}: ${errorMessage(error)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (metadataUpdateFailures === repoMetadata.size && repoMetadata.size > 0) {
|
|
224
|
+
warn(MODULE, `[ALL_METADATA_UPDATES_FAILED] All ${repoMetadata.size} metadata update(s) failed.`);
|
|
225
|
+
}
|
|
226
|
+
// Auto-sync trustedProjects from repos with merged PRs
|
|
227
|
+
let trustSyncFailures = 0;
|
|
228
|
+
for (const [repo] of mergedCounts) {
|
|
229
|
+
try {
|
|
230
|
+
stateManager.addTrustedProject(repo);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
trustSyncFailures++;
|
|
234
|
+
warn(MODULE, `Failed to sync trusted project ${repo}: ${errorMessage(error)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
238
|
+
warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
227
241
|
}
|
|
228
|
-
|
|
229
|
-
warn(MODULE, `
|
|
242
|
+
catch (error) {
|
|
243
|
+
warn(MODULE, `Failed to persist metadata/trust updates: ${errorMessage(error)}`);
|
|
230
244
|
}
|
|
231
245
|
}
|
|
232
246
|
/**
|
|
@@ -244,38 +258,47 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
244
258
|
const shelvedPRs = [];
|
|
245
259
|
const autoUnshelvedPRs = [];
|
|
246
260
|
const activePRs = [];
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
// Wrap mutations in batch: unshelvePR calls + setLastDigest produce a single save.
|
|
262
|
+
// Outer try-catch: save failure should not crash the daily check (in-memory mutations still apply).
|
|
263
|
+
try {
|
|
264
|
+
stateManager.batch(() => {
|
|
265
|
+
for (const pr of overriddenPRs) {
|
|
266
|
+
if (stateManager.isPRShelved(pr.url)) {
|
|
267
|
+
if (CRITICAL_STATUSES.has(pr.status)) {
|
|
268
|
+
stateManager.unshelvePR(pr.url);
|
|
269
|
+
autoUnshelvedPRs.push(toShelvedPRRef(pr));
|
|
270
|
+
activePRs.push(pr);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
shelvedPRs.push(toShelvedPRRef(pr));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (pr.stalenessTier === 'dormant' && !CRITICAL_STATUSES.has(pr.status)) {
|
|
277
|
+
// Dormant PRs are auto-shelved unless they need addressing
|
|
278
|
+
// (e.g. maintainer commented on a stale PR — it should resurface)
|
|
279
|
+
shelvedPRs.push(toShelvedPRRef(pr));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
activePRs.push(pr);
|
|
283
|
+
}
|
|
256
284
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
activePRs.
|
|
265
|
-
|
|
285
|
+
// Generate digest from override-applied PRs so status categories are correct.
|
|
286
|
+
// Note: digest.openPRs contains ALL fetched PRs (including shelved).
|
|
287
|
+
// We override summary fields below to reflect active-only counts.
|
|
288
|
+
const digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
|
|
289
|
+
// Attach shelve info to digest
|
|
290
|
+
digest.shelvedPRs = shelvedPRs;
|
|
291
|
+
digest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
292
|
+
digest.summary.totalActivePRs = activePRs.length;
|
|
293
|
+
// Store digest in state so dashboard can render it
|
|
294
|
+
stateManager.setLastDigest(digest);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
warn(MODULE, `Failed to persist partition state: ${errorMessage(error)}`);
|
|
266
299
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
// We override summary fields below to reflect active-only counts.
|
|
270
|
-
const digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
|
|
271
|
-
// Attach shelve info to digest
|
|
272
|
-
digest.shelvedPRs = shelvedPRs;
|
|
273
|
-
digest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
274
|
-
digest.summary.totalActivePRs = activePRs.length;
|
|
275
|
-
// Store digest in state so dashboard can render it
|
|
276
|
-
stateManager.setLastDigest(digest);
|
|
277
|
-
// Save state (updates lastRunAt, lastDigest, and any auto-unshelve changes)
|
|
278
|
-
stateManager.save();
|
|
300
|
+
// Digest was created inside batch — reconstruct from state
|
|
301
|
+
const digest = stateManager.getState().lastDigest;
|
|
279
302
|
return { activePRs, shelvedPRs, autoUnshelvedPRs, digest };
|
|
280
303
|
}
|
|
281
304
|
/**
|
|
@@ -287,43 +310,47 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
287
310
|
const stateManager = getStateManager();
|
|
288
311
|
// Assess capacity from active PRs only (shelved PRs excluded)
|
|
289
312
|
const capacity = assessCapacity(activePRs, stateManager.getState().config.maxActivePRs, shelvedPRs.length);
|
|
290
|
-
// Filter dismissed issues: suppress if dismissed after last response, resurface + auto-undismiss if new activity
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
// Filter dismissed issues: suppress if dismissed after last response, resurface + auto-undismiss if new activity.
|
|
314
|
+
// Batch: undismissIssue calls trigger autoSave — batch produces a single disk write for all auto-undismisses.
|
|
315
|
+
let filteredCommentedIssues = [];
|
|
316
|
+
try {
|
|
317
|
+
stateManager.batch(() => {
|
|
318
|
+
filteredCommentedIssues = commentedIssues.filter((issue) => {
|
|
319
|
+
const dismissedAt = stateManager.getIssueDismissedAt(issue.url);
|
|
320
|
+
if (!dismissedAt)
|
|
321
|
+
return true; // Not dismissed — include
|
|
322
|
+
if (issue.status === 'new_response') {
|
|
323
|
+
const responseTime = new Date(issue.lastResponseAt).getTime();
|
|
324
|
+
const dismissTime = new Date(dismissedAt).getTime();
|
|
325
|
+
if (isNaN(responseTime) || isNaN(dismissTime)) {
|
|
326
|
+
// Invalid timestamp — fail open (include issue to be safe) without
|
|
327
|
+
// permanently removing dismiss record (may be a transient data issue)
|
|
328
|
+
warn(MODULE, `Invalid timestamp in dismiss check for ${issue.url}, including issue`);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (responseTime > dismissTime) {
|
|
332
|
+
// New activity after dismiss — auto-undismiss and resurface
|
|
333
|
+
warn(MODULE, `Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
|
|
334
|
+
try {
|
|
335
|
+
stateManager.undismissIssue(issue.url);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
warn(MODULE, `Failed to persist auto-undismiss for ${issue.url}: ${errorMessage(error)}`);
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Still dismissed (last response is at or before dismiss timestamp)
|
|
344
|
+
return false;
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
warn(MODULE, `Failed to persist auto-undismiss state: ${errorMessage(error)}`);
|
|
350
|
+
}
|
|
316
351
|
const issueResponses = filteredCommentedIssues.filter((i) => i.status === 'new_response');
|
|
317
352
|
const summary = formatSummary(digest, capacity, issueResponses);
|
|
318
|
-
//
|
|
319
|
-
if (hasAutoUndismissed) {
|
|
320
|
-
try {
|
|
321
|
-
stateManager.save();
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
353
|
+
// Auto-undismiss mutations are auto-saved by undismissIssue()
|
|
327
354
|
const actionableIssues = collectActionableIssues(activePRs, previousLastDigestAt);
|
|
328
355
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
329
356
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
@@ -390,8 +417,16 @@ async function executeDailyCheckInternal(token) {
|
|
|
390
417
|
const { prs, failures, mergedCounts, closedCounts, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed, recentlyClosedPRs, recentlyMergedPRs, commentedIssues, } = await fetchPRData(prMonitor, token);
|
|
391
418
|
// Phase 2: Update repo scores (signals, star counts, trust sync)
|
|
392
419
|
await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
|
|
393
|
-
// Phase 3: Persist monthly analytics
|
|
394
|
-
|
|
420
|
+
// Phase 3: Persist monthly analytics (batch the 3 monthly setter calls).
|
|
421
|
+
// try-catch: analytics are supplementary — save failure should not crash the daily check.
|
|
422
|
+
try {
|
|
423
|
+
getStateManager().batch(() => {
|
|
424
|
+
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
warn(MODULE, `Failed to persist monthly analytics: ${errorMessage(error)}`);
|
|
429
|
+
}
|
|
395
430
|
// Capture lastDigestAt BEFORE Phase 4 overwrites it with the current run's timestamp.
|
|
396
431
|
// Used by collectActionableIssues to determine which PRs are "new" (created since last digest).
|
|
397
432
|
const previousLastDigestAt = getStateManager().getState().lastDigestAt;
|
|
@@ -169,43 +169,50 @@ export async function fetchDashboardData(token) {
|
|
|
169
169
|
if (failures.length > 0) {
|
|
170
170
|
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
171
171
|
}
|
|
172
|
-
//
|
|
172
|
+
// Wrap all state mutations in a batch for a single disk write.
|
|
173
|
+
// try-catch: save errors should not crash the dashboard data fetch.
|
|
173
174
|
try {
|
|
174
|
-
stateManager.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
stateManager.batch(() => {
|
|
176
|
+
// Store new merged PRs incrementally (dedupes by URL)
|
|
177
|
+
try {
|
|
178
|
+
stateManager.addMergedPRs(newMergedPRs);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
warn(MODULE, `Failed to store merged PRs: ${errorMessage(error)}`);
|
|
182
|
+
}
|
|
183
|
+
// Store new closed PRs incrementally (dedupes by URL)
|
|
184
|
+
try {
|
|
185
|
+
stateManager.addClosedPRs(newClosedPRs);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
warn(MODULE, `Failed to store closed PRs: ${errorMessage(error)}`);
|
|
189
|
+
}
|
|
190
|
+
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
191
|
+
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
192
|
+
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
193
|
+
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
194
|
+
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
195
|
+
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
196
|
+
// Dormant PRs are treated as shelved unless they need addressing
|
|
197
|
+
const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
|
|
198
|
+
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || (pr.stalenessTier === 'dormant' && pr.status !== 'needs_addressing'));
|
|
199
|
+
digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
|
|
200
|
+
digest.autoUnshelvedPRs = [];
|
|
201
|
+
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
202
|
+
stateManager.setLastDigest(digest);
|
|
203
|
+
});
|
|
182
204
|
}
|
|
183
205
|
catch (error) {
|
|
184
|
-
warn(MODULE, `Failed to
|
|
206
|
+
warn(MODULE, `Failed to persist dashboard state: ${errorMessage(error)}`);
|
|
185
207
|
}
|
|
186
|
-
|
|
208
|
+
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
209
|
+
// Convert stored PRs to full types (derive repo/number from URL) — read-only, outside batch
|
|
187
210
|
const allMergedPRs = storedToMergedPRs(stateManager.getMergedPRs());
|
|
188
211
|
const allClosedPRs = storedToClosedPRs(stateManager.getClosedPRs());
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
193
|
-
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
194
|
-
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
195
|
-
// Dormant PRs are treated as shelved unless they need addressing
|
|
196
|
-
const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
|
|
197
|
-
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || (pr.stalenessTier === 'dormant' && pr.status !== 'needs_addressing'));
|
|
198
|
-
digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
|
|
199
|
-
digest.autoUnshelvedPRs = [];
|
|
200
|
-
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
201
|
-
stateManager.setLastDigest(digest);
|
|
202
|
-
try {
|
|
203
|
-
stateManager.save();
|
|
212
|
+
const digest = stateManager.getState().lastDigest;
|
|
213
|
+
if (!digest) {
|
|
214
|
+
throw new Error('Dashboard data fetch failed: digest was not generated');
|
|
204
215
|
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
|
|
207
|
-
}
|
|
208
|
-
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
209
216
|
return { digest, commentedIssues, allMergedPRs, allClosedPRs };
|
|
210
217
|
}
|
|
211
218
|
/**
|
|
@@ -53,6 +53,13 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
|
|
|
53
53
|
const stats = buildDashboardStats(digest, state, filteredMergedPRs.length, filteredClosedPRs.length);
|
|
54
54
|
const dismissedIssues = state.config.dismissedIssues || {};
|
|
55
55
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response' && !(i.url in dismissedIssues));
|
|
56
|
+
// Build repo metadata map from repoScores — omit repos without stars or language to avoid empty entries
|
|
57
|
+
const repoMetadata = {};
|
|
58
|
+
for (const [repo, score] of Object.entries(repoScores)) {
|
|
59
|
+
if (score.stargazersCount !== undefined || score.language !== undefined) {
|
|
60
|
+
repoMetadata[repo] = { stars: score.stargazersCount, language: score.language };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
56
63
|
return {
|
|
57
64
|
stats,
|
|
58
65
|
prsByRepo,
|
|
@@ -69,6 +76,7 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
|
|
|
69
76
|
issueResponses,
|
|
70
77
|
allMergedPRs: filteredMergedPRs,
|
|
71
78
|
allClosedPRs: filteredClosedPRs,
|
|
79
|
+
repoMetadata,
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
82
|
/**
|
|
@@ -285,7 +293,6 @@ export async function startDashboardServer(options) {
|
|
|
285
293
|
else {
|
|
286
294
|
// dismiss_issue_response
|
|
287
295
|
stateManager.dismissIssue(body.url, new Date().toISOString());
|
|
288
|
-
stateManager.save();
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
catch (error) {
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -10,9 +10,6 @@ export async function runDismiss(options) {
|
|
|
10
10
|
validateGitHubUrl(options.url, ISSUE_URL_PATTERN, 'issue');
|
|
11
11
|
const stateManager = getStateManager();
|
|
12
12
|
const added = stateManager.dismissIssue(options.url, new Date().toISOString());
|
|
13
|
-
if (added) {
|
|
14
|
-
stateManager.save();
|
|
15
|
-
}
|
|
16
13
|
return { dismissed: added, url: options.url };
|
|
17
14
|
}
|
|
18
15
|
export async function runUndismiss(options) {
|
|
@@ -20,8 +17,5 @@ export async function runUndismiss(options) {
|
|
|
20
17
|
validateGitHubUrl(options.url, ISSUE_URL_PATTERN, 'issue');
|
|
21
18
|
const stateManager = getStateManager();
|
|
22
19
|
const removed = stateManager.undismissIssue(options.url);
|
|
23
|
-
if (removed) {
|
|
24
|
-
stateManager.save();
|
|
25
|
-
}
|
|
26
20
|
return { undismissed: removed, url: options.url };
|
|
27
21
|
}
|