@oss-autopilot/core 3.13.4 → 3.14.1
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/README.md +3 -3
- package/dist/cli-registry.js +59 -84
- package/dist/cli.bundle.cjs +112 -109
- package/dist/cli.js +5 -4
- package/dist/commands/comments.js +44 -10
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +50 -2
- package/dist/commands/curated-list.d.ts +17 -0
- package/dist/commands/curated-list.js +25 -0
- package/dist/commands/daily.d.ts +7 -1
- package/dist/commands/daily.js +136 -57
- package/dist/commands/dashboard-cache.d.ts +69 -0
- package/dist/commands/dashboard-cache.js +219 -0
- package/dist/commands/dashboard-data.d.ts +18 -10
- package/dist/commands/dashboard-data.js +58 -8
- package/dist/commands/dashboard-gist-sync.d.ts +93 -0
- package/dist/commands/dashboard-gist-sync.js +237 -0
- package/dist/commands/dashboard-server.d.ts +6 -10
- package/dist/commands/dashboard-server.js +181 -347
- package/dist/commands/features.js +6 -0
- package/dist/commands/guidelines.d.ts +6 -0
- package/dist/commands/guidelines.js +7 -0
- package/dist/commands/index.d.ts +2 -5
- package/dist/commands/index.js +2 -4
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +7 -1
- package/dist/commands/list-mark-done.js +6 -21
- package/dist/commands/list-move-tier.js +3 -5
- package/dist/commands/locate-issue-list.d.ts +25 -0
- package/dist/commands/locate-issue-list.js +67 -0
- package/dist/commands/merge-loop.d.ts +63 -0
- package/dist/commands/merge-loop.js +157 -0
- package/dist/commands/repo-vet.js +40 -1
- package/dist/commands/scout-bridge.d.ts +35 -2
- package/dist/commands/scout-bridge.js +65 -13
- package/dist/commands/search.d.ts +4 -6
- package/dist/commands/search.js +58 -11
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +56 -2
- package/dist/commands/skip-file-parser.d.ts +23 -0
- package/dist/commands/skip-file-parser.js +23 -10
- package/dist/commands/startup.d.ts +1 -6
- package/dist/commands/startup.js +25 -59
- package/dist/commands/track.d.ts +2 -2
- package/dist/commands/track.js +2 -2
- package/dist/commands/vet-list.d.ts +6 -6
- package/dist/commands/vet-list.js +194 -65
- package/dist/core/config-registry.js +36 -0
- package/dist/core/daily-logic.d.ts +25 -2
- package/dist/core/daily-logic.js +58 -3
- package/dist/core/gist-health.d.ts +81 -0
- package/dist/core/gist-health.js +39 -0
- package/dist/core/gist-state-store.d.ts +3 -1
- package/dist/core/gist-state-store.js +7 -2
- package/dist/core/github-stats.d.ts +2 -2
- package/dist/core/github-stats.js +20 -4
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +5 -4
- package/dist/core/issue-conversation.js +8 -2
- package/dist/core/issue-grading.d.ts +9 -0
- package/dist/core/issue-grading.js +9 -0
- package/dist/core/issue-verification.d.ts +39 -0
- package/dist/core/issue-verification.js +48 -0
- package/dist/core/pagination.d.ts +27 -0
- package/dist/core/pagination.js +23 -5
- package/dist/core/pr-comments-fetcher.d.ts +7 -0
- package/dist/core/pr-comments-fetcher.js +19 -8
- package/dist/core/pr-monitor.d.ts +2 -0
- package/dist/core/pr-monitor.js +26 -9
- package/dist/core/repo-score-manager.d.ts +2 -2
- package/dist/core/repo-score-manager.js +3 -3
- package/dist/core/repo-vet.d.ts +2 -2
- package/dist/core/repo-vet.js +1 -1
- package/dist/core/review-analysis.d.ts +19 -0
- package/dist/core/review-analysis.js +28 -0
- package/dist/core/state-schema.d.ts +43 -6
- package/dist/core/state-schema.js +81 -4
- package/dist/core/state.d.ts +36 -5
- package/dist/core/state.js +177 -28
- package/dist/core/strategy.js +6 -5
- package/dist/core/types.d.ts +8 -0
- package/dist/core/untrusted-content.d.ts +45 -0
- package/dist/core/untrusted-content.js +54 -0
- package/dist/formatters/json.d.ts +120 -12
- package/dist/formatters/json.js +55 -2
- package/package.json +2 -2
- package/dist/commands/shelve.d.ts +0 -45
- package/dist/commands/shelve.js +0 -54
package/dist/commands/daily.js
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
* Domain logic lives in src/core/daily-logic.ts; this file is a thin
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
|
-
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, summarizeAttentionBuckets, } from '../core/index.js';
|
|
9
|
+
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, renderGistWarning, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, summarizeAttentionBuckets, } from '../core/index.js';
|
|
10
10
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
11
11
|
import { wrapUntrustedContent } from '../core/untrusted-content.js';
|
|
12
12
|
import { computeStrategy, shouldComputeStrategy } from '../core/strategy.js';
|
|
13
13
|
import { warn } from '../core/logger.js';
|
|
14
14
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
15
|
-
import { createAutopilotScout } from './scout-bridge.js';
|
|
16
15
|
import { updateMonthlyAnalytics } from './dashboard-data.js';
|
|
17
16
|
import { deduplicateDigest, compactActionableIssues, compactRepoGroups, buildStalenessWarning, } from '../formatters/json.js';
|
|
17
|
+
import { reconcileMergedPRsWithList } from './merge-loop.js';
|
|
18
18
|
const MODULE = 'daily';
|
|
19
19
|
/**
|
|
20
20
|
* Record a non-fatal failure: push a structured entry into the run's warnings
|
|
@@ -45,7 +45,7 @@ export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacit
|
|
|
45
45
|
// reuse it without crossing the commands → sibling-command boundary
|
|
46
46
|
// (#1208 M7). Re-exported here for backward compatibility with anyone
|
|
47
47
|
// importing from this module.
|
|
48
|
-
import { buildStarFilter } from '../core/daily-logic.js';
|
|
48
|
+
import { buildStarFilter, firstMaintainerResponseFromDigest } from '../core/daily-logic.js';
|
|
49
49
|
export { buildStarFilter };
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// Phase functions
|
|
@@ -192,16 +192,17 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts, warn
|
|
|
192
192
|
warn(MODULE, `Failed to update merged count for ${repo}: ${errorMessage(error)}`);
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
-
if (mergedCountFailures
|
|
196
|
-
//
|
|
197
|
-
// succeeded, but
|
|
198
|
-
// silently stale — surface it as
|
|
195
|
+
if (mergedCountFailures > 0) {
|
|
196
|
+
// Partial or total failure (#1448): the batch outer-catch sees nothing
|
|
197
|
+
// because the batch itself succeeded, but individual mutations inside
|
|
198
|
+
// threw. The affected repos' scores are silently stale — surface it as
|
|
199
|
+
// a warning distinct from the outer catch.
|
|
199
200
|
warnings.push({
|
|
200
201
|
phase: 'repo-scores',
|
|
201
202
|
operation: 'update merged counts',
|
|
202
|
-
message:
|
|
203
|
+
message: `${mergedCountFailures} of ${mergedCounts.size} merged count update(s) failed. This may indicate corrupted state.`,
|
|
203
204
|
});
|
|
204
|
-
warn(MODULE, `[
|
|
205
|
+
warn(MODULE, `[MERGED_COUNT_UPDATES_FAILED] ${mergedCountFailures} of ${mergedCounts.size} merged count update(s) failed.`);
|
|
205
206
|
}
|
|
206
207
|
// Populate closedWithoutMergeCount in repo scores.
|
|
207
208
|
const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
|
|
@@ -218,13 +219,13 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts, warn
|
|
|
218
219
|
warn(MODULE, `Failed to update closed count for ${repo}: ${errorMessage(error)}`);
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
|
-
if (closedCountFailures
|
|
222
|
+
if (closedCountFailures > 0) {
|
|
222
223
|
warnings.push({
|
|
223
224
|
phase: 'repo-scores',
|
|
224
225
|
operation: 'update closed counts',
|
|
225
|
-
message:
|
|
226
|
+
message: `${closedCountFailures} of ${closedCounts.size} closed count update(s) failed. This may indicate corrupted state.`,
|
|
226
227
|
});
|
|
227
|
-
warn(MODULE, `[
|
|
228
|
+
warn(MODULE, `[CLOSED_COUNT_UPDATES_FAILED] ${closedCountFailures} of ${closedCounts.size} closed count update(s) failed.`);
|
|
228
229
|
}
|
|
229
230
|
// Update repo signals from observed open PR data
|
|
230
231
|
const repoSignals = computeRepoSignals(prs);
|
|
@@ -238,13 +239,13 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts, warn
|
|
|
238
239
|
warn(MODULE, `Failed to update signals for ${repo}: ${errorMessage(error)}`);
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
|
-
if (signalUpdateFailures
|
|
242
|
+
if (signalUpdateFailures > 0) {
|
|
242
243
|
warnings.push({
|
|
243
244
|
phase: 'repo-scores',
|
|
244
245
|
operation: 'update repo signals',
|
|
245
|
-
message:
|
|
246
|
+
message: `${signalUpdateFailures} of ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`,
|
|
246
247
|
});
|
|
247
|
-
warn(MODULE, `[
|
|
248
|
+
warn(MODULE, `[SIGNAL_UPDATES_FAILED] ${signalUpdateFailures} of ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
|
|
248
249
|
}
|
|
249
250
|
});
|
|
250
251
|
}
|
|
@@ -277,13 +278,13 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts, warn
|
|
|
277
278
|
warn(MODULE, `Failed to update metadata for ${repo}: ${errorMessage(error)}`);
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
|
-
if (metadataUpdateFailures
|
|
281
|
+
if (metadataUpdateFailures > 0) {
|
|
281
282
|
warnings.push({
|
|
282
283
|
phase: 'repo-scores',
|
|
283
284
|
operation: 'update repo metadata',
|
|
284
|
-
message:
|
|
285
|
+
message: `${metadataUpdateFailures} of ${repoMetadata.size} metadata update(s) failed. This may indicate corrupted state.`,
|
|
285
286
|
});
|
|
286
|
-
warn(MODULE, `[
|
|
287
|
+
warn(MODULE, `[METADATA_UPDATES_FAILED] ${metadataUpdateFailures} of ${repoMetadata.size} metadata update(s) failed.`);
|
|
287
288
|
}
|
|
288
289
|
// Auto-sync trustedProjects from repos with merged PRs
|
|
289
290
|
let trustSyncFailures = 0;
|
|
@@ -296,13 +297,13 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts, warn
|
|
|
296
297
|
warn(MODULE, `Failed to sync trusted project ${repo}: ${errorMessage(error)}`);
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
|
-
if (trustSyncFailures
|
|
300
|
+
if (trustSyncFailures > 0) {
|
|
300
301
|
warnings.push({
|
|
301
302
|
phase: 'repo-scores',
|
|
302
303
|
operation: 'sync trusted projects',
|
|
303
|
-
message:
|
|
304
|
+
message: `${trustSyncFailures} of ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`,
|
|
304
305
|
});
|
|
305
|
-
warn(MODULE, `[
|
|
306
|
+
warn(MODULE, `[TRUST_SYNCS_FAILED] ${trustSyncFailures} of ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
|
|
306
307
|
}
|
|
307
308
|
});
|
|
308
309
|
}
|
|
@@ -320,13 +321,20 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs, warn
|
|
|
320
321
|
// Apply dashboard/CLI status overrides before partitioning.
|
|
321
322
|
// This ensures PRs reclassified in the dashboard (e.g., "Need Attention" → "Waiting")
|
|
322
323
|
// are respected by the CLI pipeline.
|
|
323
|
-
|
|
324
|
+
// Override-application failures (#1448) mean a PR silently shows its
|
|
325
|
+
// un-overridden status — record each into the run's warnings[].
|
|
326
|
+
const overrideFailures = [];
|
|
327
|
+
const overriddenPRs = applyStatusOverrides(prs, stateManager.getState(), overrideFailures);
|
|
328
|
+
for (const failure of overrideFailures) {
|
|
329
|
+
recordWarning(warnings, 'partition', 'apply status override', null, failure);
|
|
330
|
+
}
|
|
324
331
|
// Partition PRs into active vs shelved, auto-unshelving when maintainers engage
|
|
325
332
|
const shelvedPRs = [];
|
|
326
333
|
const autoUnshelvedPRs = [];
|
|
327
334
|
const activePRs = [];
|
|
328
335
|
// Wrap mutations in batch: unshelvePR calls + setLastDigest produce a single save.
|
|
329
336
|
// Outer try-catch: save failure should not crash the daily check (in-memory mutations still apply).
|
|
337
|
+
let digest;
|
|
330
338
|
try {
|
|
331
339
|
stateManager.batch(() => {
|
|
332
340
|
for (const pr of overriddenPRs) {
|
|
@@ -349,23 +357,49 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs, warn
|
|
|
349
357
|
activePRs.push(pr);
|
|
350
358
|
}
|
|
351
359
|
}
|
|
352
|
-
// Generate digest from override-applied PRs so
|
|
360
|
+
// Generate the in-memory digest from override-applied PRs so daily's own
|
|
361
|
+
// status categories (needsAddressing/waiting partitions, counts, JSON
|
|
362
|
+
// output) reflect dashboard/CLI reclassifications.
|
|
353
363
|
// Note: digest.openPRs contains ALL fetched PRs (including shelved).
|
|
354
364
|
// We override summary fields below to reflect active-only counts.
|
|
355
|
-
|
|
365
|
+
digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
|
|
356
366
|
// Attach shelve info to digest
|
|
357
367
|
digest.shelvedPRs = shelvedPRs;
|
|
358
368
|
digest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
359
369
|
digest.summary.totalActivePRs = activePRs.length;
|
|
360
|
-
//
|
|
361
|
-
|
|
370
|
+
// The PERSISTED digest keeps RAW statuses (#1445): it seeds the
|
|
371
|
+
// dashboard server's cached rebuild source, and applyStatusOverrides
|
|
372
|
+
// can only apply overrides, never un-apply baked ones — persisting the
|
|
373
|
+
// override-applied digest would make CLEARING an override (move
|
|
374
|
+
// target=auto) a silent no-op whenever the dashboard's background
|
|
375
|
+
// refresh fails. Mirrors the raw-digest contract in dashboard-data.ts
|
|
376
|
+
// (which also attaches override-derived shelve info to a raw digest).
|
|
377
|
+
const rawDigest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
378
|
+
rawDigest.shelvedPRs = shelvedPRs;
|
|
379
|
+
rawDigest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
380
|
+
rawDigest.summary.totalActivePRs = activePRs.length;
|
|
381
|
+
stateManager.setLastDigest(rawDigest);
|
|
362
382
|
});
|
|
363
383
|
}
|
|
364
384
|
catch (error) {
|
|
365
385
|
recordWarning(warnings, 'partition', 'persist partition state', error);
|
|
366
386
|
}
|
|
367
|
-
//
|
|
368
|
-
|
|
387
|
+
// If digest generation threw inside the batch, fall back to the persisted
|
|
388
|
+
// digest (mirrors the previous read-back-from-state behavior). The
|
|
389
|
+
// recordWarning call above already flagged the run as degraded.
|
|
390
|
+
if (digest === undefined) {
|
|
391
|
+
const persisted = stateManager.getState().lastDigest;
|
|
392
|
+
if (!persisted) {
|
|
393
|
+
// First-ever run with no persisted digest to fall back to: fail loudly
|
|
394
|
+
// with the recorded reason instead of letting Phase 5 die on an
|
|
395
|
+
// unrelated TypeError at `digest.summary` (#1456). Fabricating an
|
|
396
|
+
// empty digest here would silently report "no PRs" — dishonest.
|
|
397
|
+
const recorded = warnings.find((w) => w.phase === 'partition');
|
|
398
|
+
throw new Error(`Daily digest generation failed and no previously persisted digest exists to fall back to` +
|
|
399
|
+
(recorded ? `: ${recorded.message}` : '.'));
|
|
400
|
+
}
|
|
401
|
+
return { activePRs, shelvedPRs, autoUnshelvedPRs, digest: persisted };
|
|
402
|
+
}
|
|
369
403
|
return { activePRs, shelvedPRs, autoUnshelvedPRs, digest };
|
|
370
404
|
}
|
|
371
405
|
/**
|
|
@@ -373,7 +407,7 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs, warn
|
|
|
373
407
|
* Assesses capacity, filters dismissed issues, computes actionable items,
|
|
374
408
|
* and assembles the action menu.
|
|
375
409
|
*/
|
|
376
|
-
function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, warnings, previousLastDigestAt) {
|
|
410
|
+
function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, warnings, previousLastDigestAt, unextractedMergeCount) {
|
|
377
411
|
const stateManager = getStateManager();
|
|
378
412
|
// Assess capacity from active PRs only (shelved PRs excluded)
|
|
379
413
|
const capacity = assessCapacity(activePRs, stateManager.getState().config.maxActivePRs, shelvedPRs.length);
|
|
@@ -424,7 +458,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
424
458
|
// the same buckets per-PR, so the headline counts cannot diverge.
|
|
425
459
|
const attention = summarizeAttentionBuckets(activePRs);
|
|
426
460
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length, attention);
|
|
427
|
-
const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
|
|
461
|
+
const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues, attention, unextractedMergeCount);
|
|
428
462
|
const repoGroups = groupPRsByRepo(activePRs);
|
|
429
463
|
// Periodic strategy snapshot (#1270 Step 2). Cadence-gated to fire every
|
|
430
464
|
// 30 days OR after 5+ PRs merge since the last snapshot, whichever comes
|
|
@@ -513,6 +547,9 @@ export function toDailyOutput(result) {
|
|
|
513
547
|
failures: result.failures,
|
|
514
548
|
warnings: result.warnings,
|
|
515
549
|
strategySummary: result.strategySummary,
|
|
550
|
+
// Conditional spread (not a plain assignment) so merge-free runs carry
|
|
551
|
+
// no `listUpdates` key at all — serialized output and goldens unchanged.
|
|
552
|
+
...(result.listUpdates ? { listUpdates: result.listUpdates } : {}),
|
|
516
553
|
};
|
|
517
554
|
}
|
|
518
555
|
/**
|
|
@@ -547,13 +584,15 @@ async function executeDailyCheckInternal(token) {
|
|
|
547
584
|
// callgraph documents which phases can produce non-fatal warnings. See #1042.
|
|
548
585
|
const warnings = [];
|
|
549
586
|
// Surface gist-mode degradation in the machine-readable envelope (#1431):
|
|
550
|
-
// a process
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
587
|
+
// a degraded process (local-only under a gist config, or a #1443 degraded
|
|
588
|
+
// bootstrap) writes mutations that will not sync. Previously the only
|
|
589
|
+
// signal was a stderr warn, invisible to --json consumers. Predicate and
|
|
590
|
+
// prose both come from the one health source (#1444). When this warning
|
|
591
|
+
// is present, the JSON envelope suppresses its process-level
|
|
592
|
+
// `gistInitWarning` duplicate — see `jsonSuccess` in formatters/json.ts.
|
|
593
|
+
const gistHealth = getStateManager().getGistHealth();
|
|
594
|
+
if (gistHealth.degraded) {
|
|
595
|
+
recordWarning(warnings, 'gist-init', 'Gist persistence degraded', null, renderGistWarning(gistHealth.degraded.cause));
|
|
557
596
|
}
|
|
558
597
|
// Surface Gist staleness up-front so consumers see it even if Phase 1 fails (#1193).
|
|
559
598
|
const staleness = getStateManager().getStateStaleness();
|
|
@@ -574,46 +613,86 @@ async function executeDailyCheckInternal(token) {
|
|
|
574
613
|
// Phase 3: Persist monthly analytics and store merged/closed PR history.
|
|
575
614
|
// try-catch: analytics are supplementary — save failure should not crash the daily check.
|
|
576
615
|
try {
|
|
616
|
+
// Previous run's digest — read BEFORE Phase 4 overwrites it. If a
|
|
617
|
+
// just-merged/closed PR was open during a prior enriched run, its
|
|
618
|
+
// openPRs entry carries firstMaintainerResponseAt for the ledger (#1461).
|
|
619
|
+
const previousDigest = getStateManager().getState().lastDigest;
|
|
577
620
|
getStateManager().batch(() => {
|
|
578
|
-
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
621
|
+
const analyticsFailures = updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
622
|
+
// Per-metric setter failures are caught inside updateMonthlyAnalytics
|
|
623
|
+
// (one bad metric must not sink the others); surface each in the run's
|
|
624
|
+
// warnings[] instead of dropping the returned labels (#1447).
|
|
625
|
+
for (const failure of analyticsFailures) {
|
|
626
|
+
recordWarning(warnings, 'analytics', failure, null, 'persist failed');
|
|
627
|
+
}
|
|
579
628
|
// Store recently merged/closed PRs in the persistent arrays.
|
|
580
629
|
// This ensures the mergedPRs/closedPRs ledger is populated even when
|
|
581
630
|
// the dashboard is never opened (which has its own fetch path).
|
|
582
|
-
// addMergedPRs/addClosedPRs deduplicate by URL
|
|
631
|
+
// addMergedPRs/addClosedPRs deduplicate by URL (re-seen entries upgrade
|
|
632
|
+
// missing ledger fields in place), so overlaps are safe. openedAt comes
|
|
633
|
+
// free from the search result; firstMaintainerResponseAt is best-effort
|
|
634
|
+
// from the previous run's enriched digest — zero new API calls (#1461).
|
|
583
635
|
if (recentlyMergedPRs.length > 0) {
|
|
584
|
-
getStateManager().addMergedPRs(recentlyMergedPRs.map((pr) => ({
|
|
636
|
+
getStateManager().addMergedPRs(recentlyMergedPRs.map((pr) => ({
|
|
637
|
+
url: pr.url,
|
|
638
|
+
title: pr.title,
|
|
639
|
+
mergedAt: pr.mergedAt,
|
|
640
|
+
openedAt: pr.openedAt,
|
|
641
|
+
firstMaintainerResponseAt: firstMaintainerResponseFromDigest(previousDigest, pr.url),
|
|
642
|
+
})));
|
|
585
643
|
}
|
|
586
644
|
if (recentlyClosedPRs.length > 0) {
|
|
587
|
-
getStateManager().addClosedPRs(recentlyClosedPRs.map((pr) => ({
|
|
645
|
+
getStateManager().addClosedPRs(recentlyClosedPRs.map((pr) => ({
|
|
646
|
+
url: pr.url,
|
|
647
|
+
title: pr.title,
|
|
648
|
+
closedAt: pr.closedAt,
|
|
649
|
+
openedAt: pr.openedAt,
|
|
650
|
+
firstMaintainerResponseAt: firstMaintainerResponseFromDigest(previousDigest, pr.url),
|
|
651
|
+
})));
|
|
588
652
|
}
|
|
589
653
|
});
|
|
590
654
|
}
|
|
591
655
|
catch (error) {
|
|
592
656
|
recordWarning(warnings, 'analytics', 'persist monthly analytics', error);
|
|
593
657
|
}
|
|
594
|
-
// Phase 3.5:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
658
|
+
// Phase 3.5: Close the merge loop (#1463). Join detected merges against
|
|
659
|
+
// the curated issue list (the PR URL recorded in an entry's sub-bullets
|
|
660
|
+
// is the only deterministic PR→issue link that exists — see
|
|
661
|
+
// merge-loop.ts) and auto-mark matching entries done. Safe to auto-run:
|
|
662
|
+
// the PR is MERGED per GitHub, and already-marked entries are quiet
|
|
663
|
+
// no-ops. Failures land in warnings[] via the shared collector.
|
|
664
|
+
const listUpdates = reconcileMergedPRsWithList({ mergedPRs: recentlyMergedPRs, warnings });
|
|
665
|
+
// Count freshly-merged PRs whose ledger entry has no learnings extraction
|
|
666
|
+
// stamp yet (#867 plumbing). Drives the action menu's extract_learnings
|
|
667
|
+
// nudge: it stays up for the whole recently-merged window (7 days) until
|
|
668
|
+
// the user runs the extraction, then self-clears.
|
|
669
|
+
let unextractedMergeCount = 0;
|
|
670
|
+
try {
|
|
671
|
+
const ledger = getStateManager().getState().mergedPRs ?? [];
|
|
672
|
+
const recentUrls = new Set(recentlyMergedPRs.map((pr) => pr.url));
|
|
673
|
+
unextractedMergeCount = ledger.filter((pr) => recentUrls.has(pr.url) && !pr.learningsExtractedAt).length;
|
|
609
674
|
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
recordWarning(warnings, 'merge-loop', 'count unextracted merged PRs', error);
|
|
677
|
+
}
|
|
678
|
+
// No scout pass here (#1458): the former Phase 3.5 fed recentlyMerged/ClosedPRs
|
|
679
|
+
// to an oss-scout instance and called scout.checkpoint(). Both halves were dead:
|
|
680
|
+
// scout's recordMergedPR/recordClosedPR deduplicate by URL against the ledger
|
|
681
|
+
// that Phase 3 just wrote (buildScoutState maps state.mergedPRs/closedPRs into
|
|
682
|
+
// the scout state), so they returned before touching any repo score; and under
|
|
683
|
+
// persistence:'provided' checkpoint() has no gist store and no local store, so
|
|
684
|
+
// it persisted nothing. Cross-tool sharing happens through buildScoutState
|
|
685
|
+
// reading AgentState, not through pushing events at scout.
|
|
610
686
|
// Capture lastDigestAt BEFORE Phase 4 overwrites it with the current run's timestamp.
|
|
611
687
|
// Used by collectActionableIssues to determine which PRs are "new" (created since last digest).
|
|
612
688
|
const previousLastDigestAt = getStateManager().getState().lastDigestAt;
|
|
613
689
|
// Phase 4: Partition PRs, generate and save digest
|
|
614
690
|
const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs, warnings);
|
|
615
691
|
// Phase 5: Build structured output (capacity, dismiss filter, action menu)
|
|
616
|
-
const result = generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, warnings, previousLastDigestAt);
|
|
692
|
+
const result = generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, warnings, previousLastDigestAt, unextractedMergeCount);
|
|
693
|
+
if (listUpdates) {
|
|
694
|
+
result.listUpdates = listUpdates;
|
|
695
|
+
}
|
|
617
696
|
// Checkpoint: push state to Gist if in Gist mode.
|
|
618
697
|
// If getStateManagerAsync was not called before this command ran,
|
|
619
698
|
// isGistMode() will be false and checkpoint is correctly skipped.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DashboardCache — the dashboard server's cached-response state.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from dashboard-server.ts (#1457). Owns the cached digest,
|
|
5
|
+
* commented issues, partialFailures, built JSON payload, issue-list mtime
|
|
6
|
+
* and the last background-refresh error, plus buildDashboardJson (the
|
|
7
|
+
* payload builder the cache exists to memoize). The HTTP handlers consume
|
|
8
|
+
* it as a collaborator; nothing here touches req/res.
|
|
9
|
+
*/
|
|
10
|
+
import { type DashboardJsonData } from './dashboard-data.js';
|
|
11
|
+
import { type DailyDigest, type AgentState, type CommentedIssue, type MergedPR, type ClosedPR } from '../core/types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Get the mtime of the vetted issue list file in ms, or null if unknown.
|
|
14
|
+
* Used to detect external edits and invalidate the cached dashboard payload.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getIssueListMtimeMs(): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
19
|
+
*
|
|
20
|
+
* Exported for unit testing of response-shape concerns that the full
|
|
21
|
+
* handler harness can't reach (it bakes a stale cachedDigest at server
|
|
22
|
+
* start-up, so tests that need a specific digest should call this directly).
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildDashboardJson(digest: DailyDigest, state: Readonly<AgentState>, commentedIssues: CommentedIssue[], allMergedPRs?: MergedPR[], allClosedPRs?: ClosedPR[], partialFailures?: string[]): DashboardJsonData;
|
|
25
|
+
/** The slice of DashboardFetchResult the cache adopts after a full refresh. */
|
|
26
|
+
export interface DashboardFetchSnapshot {
|
|
27
|
+
digest: DailyDigest;
|
|
28
|
+
commentedIssues: CommentedIssue[];
|
|
29
|
+
partialFailures: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare class DashboardCache {
|
|
32
|
+
/** Decorates a partialFailures payload with the gist-sync lifecycle's
|
|
33
|
+
* warnings/banners (GistSyncCoordinator.withPendingGistWarnings) on
|
|
34
|
+
* every rebuild — injected so the two lifecycles stay uncoupled. */
|
|
35
|
+
private readonly mergeWarnings;
|
|
36
|
+
private cachedDigest;
|
|
37
|
+
private cachedCommentedIssues;
|
|
38
|
+
private cachedPartialFailures;
|
|
39
|
+
private cachedJsonData;
|
|
40
|
+
private cachedIssueListMtimeMs;
|
|
41
|
+
lastBackgroundRefreshError: string | null;
|
|
42
|
+
constructor(initialDigest: DailyDigest,
|
|
43
|
+
/** Decorates a partialFailures payload with the gist-sync lifecycle's
|
|
44
|
+
* warnings/banners (GistSyncCoordinator.withPendingGistWarnings) on
|
|
45
|
+
* every rebuild — injected so the two lifecycles stay uncoupled. */
|
|
46
|
+
mergeWarnings: (failures: string[] | undefined) => string[] | undefined);
|
|
47
|
+
get digest(): DailyDigest;
|
|
48
|
+
get jsonData(): DashboardJsonData;
|
|
49
|
+
get issueListMtimeMs(): number | null;
|
|
50
|
+
/** Adopt a full fetchDashboardData result: replace the digest and
|
|
51
|
+
* commented issues unconditionally, and update the persistent banner
|
|
52
|
+
* signal — clear on a clean refresh, set when one or more sub-fetches
|
|
53
|
+
* degraded. See #1035. */
|
|
54
|
+
adoptFetchResult(result: DashboardFetchSnapshot): void;
|
|
55
|
+
/** A gist pull wholesale-replaces state, and the pulled state.lastDigest
|
|
56
|
+
* may be NEWER than this server's long-lived cachedDigest (another machine
|
|
57
|
+
* ran its daily check). Adopt it when its generatedAt is strictly newer so
|
|
58
|
+
* cross-machine results appear without a manual full refresh (#1446
|
|
59
|
+
* item 6). Unparsable timestamps compare as NaN and never adopt —
|
|
60
|
+
* conservative: keep what we have. */
|
|
61
|
+
adoptNewerPulledDigest(pulled: DailyDigest | undefined): void;
|
|
62
|
+
/** Rebuild the cached JSON payload from the cached digest + the given
|
|
63
|
+
* state, then stamp the issue-list mtime the payload reflects. Throws
|
|
64
|
+
* when buildDashboardJson does — on failure neither the payload nor the
|
|
65
|
+
* mtime is updated, so the next request retries the rebuild. `issueListMtimeMs`
|
|
66
|
+
* lets GET /api/data stamp the value it compared against (#924); the other
|
|
67
|
+
* call sites re-stat after the build. */
|
|
68
|
+
rebuild(state: Readonly<AgentState>, allMergedPRs?: MergedPR[], allClosedPRs?: ClosedPR[], issueListMtimeMs?: number | null): void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DashboardCache — the dashboard server's cached-response state.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from dashboard-server.ts (#1457). Owns the cached digest,
|
|
5
|
+
* commented issues, partialFailures, built JSON payload, issue-list mtime
|
|
6
|
+
* and the last background-refresh error, plus buildDashboardJson (the
|
|
7
|
+
* payload builder the cache exists to memoize). The HTTP handlers consume
|
|
8
|
+
* it as a collaborator; nothing here touches req/res.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import { getStateManager, applyStatusOverrides, classifyAttentionBucket } from '../core/index.js';
|
|
12
|
+
import { errorMessage } from '../core/errors.js';
|
|
13
|
+
import { warn } from '../core/logger.js';
|
|
14
|
+
import { computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, reconcileShelvePartition, storedToMergedPRs, storedToClosedPRs, } from './dashboard-data.js';
|
|
15
|
+
import { detectIssueList } from './startup.js';
|
|
16
|
+
import { parseIssueList } from './parse-list.js';
|
|
17
|
+
import { isBelowMinStars, } from '../core/types.js';
|
|
18
|
+
const MODULE = 'dashboard-server';
|
|
19
|
+
/**
|
|
20
|
+
* Read and parse the vetted issue list file (non-fatal on failure).
|
|
21
|
+
*
|
|
22
|
+
* @param failures - Optional collector (#1448): a read/parse failure pushes a
|
|
23
|
+
* label so the SPA's partialFailures banner shows the panel is degraded
|
|
24
|
+
* rather than silently empty. "No list detected" is not a failure and
|
|
25
|
+
* pushes nothing.
|
|
26
|
+
*/
|
|
27
|
+
function readVettedIssues(failures) {
|
|
28
|
+
try {
|
|
29
|
+
const info = detectIssueList();
|
|
30
|
+
if (!info)
|
|
31
|
+
return null;
|
|
32
|
+
const content = fs.readFileSync(info.path, 'utf8');
|
|
33
|
+
return parseIssueList(content);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
warn(MODULE, `Failed to read vetted issue list: ${errorMessage(error)}`);
|
|
37
|
+
failures?.push('read vetted issue list');
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the mtime of the vetted issue list file in ms, or null if unknown.
|
|
43
|
+
* Used to detect external edits and invalidate the cached dashboard payload.
|
|
44
|
+
*/
|
|
45
|
+
export function getIssueListMtimeMs() {
|
|
46
|
+
try {
|
|
47
|
+
const info = detectIssueList();
|
|
48
|
+
if (!info)
|
|
49
|
+
return null;
|
|
50
|
+
return fs.statSync(info.path).mtimeMs;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
58
|
+
*
|
|
59
|
+
* Exported for unit testing of response-shape concerns that the full
|
|
60
|
+
* handler harness can't reach (it bakes a stale cachedDigest at server
|
|
61
|
+
* start-up, so tests that need a specific digest should call this directly).
|
|
62
|
+
*/
|
|
63
|
+
export function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClosedPRs, partialFailures) {
|
|
64
|
+
// Collect build-local failures (#1448) alongside the caller-provided ones
|
|
65
|
+
// so degradations detected during THIS build (vetted-list read failure,
|
|
66
|
+
// status-override application failure) reach the SPA's partialFailures
|
|
67
|
+
// banner instead of living only in stderr.
|
|
68
|
+
const buildFailures = [...(partialFailures ?? [])];
|
|
69
|
+
// Apply status overrides ONCE, before the shelve partition is derived, so
|
|
70
|
+
// the dashboard partitions on the same post-override status the CLI
|
|
71
|
+
// partitions on (#1416). This also covers overrides set AFTER the digest
|
|
72
|
+
// was cached (a dashboard move stores an override; the action path rebuilds
|
|
73
|
+
// from the cached digest). Work on a copy — the caller's cached digest must
|
|
74
|
+
// not accumulate per-request derivations.
|
|
75
|
+
const overriddenDigest = {
|
|
76
|
+
...digest,
|
|
77
|
+
openPRs: applyStatusOverrides(digest.openPRs || [], state, buildFailures),
|
|
78
|
+
summary: { ...digest.summary },
|
|
79
|
+
};
|
|
80
|
+
// Re-derive the shelve partition from the CURRENT state before reading it.
|
|
81
|
+
// The POST /api/action path rebuilds with a cached digest whose shelvedPRs
|
|
82
|
+
// predates the shelve/unshelve, so without this the SPA action appears to do
|
|
83
|
+
// nothing until the next full /api/refresh.
|
|
84
|
+
reconcileShelvePartition(overriddenDigest, state);
|
|
85
|
+
const prsByRepo = computePRsByRepo(overriddenDigest, state);
|
|
86
|
+
const topRepos = computeTopRepos(prsByRepo);
|
|
87
|
+
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
88
|
+
// Derive from state if not provided (e.g. initial load from cached state)
|
|
89
|
+
const mergedPRs = allMergedPRs ?? storedToMergedPRs(getStateManager().getMergedPRs());
|
|
90
|
+
const closedPRs = allClosedPRs ?? storedToClosedPRs(getStateManager().getClosedPRs());
|
|
91
|
+
// Filter out PRs from repos below the minStars threshold
|
|
92
|
+
const minStars = state.config.minStars ?? 50;
|
|
93
|
+
const repoScores = state.repoScores || {};
|
|
94
|
+
const isAboveMinStars = (pr) => !isBelowMinStars(repoScores[pr.repo]?.stargazersCount, minStars);
|
|
95
|
+
const filteredMergedPRs = mergedPRs.filter(isAboveMinStars);
|
|
96
|
+
const filteredClosedPRs = closedPRs.filter(isAboveMinStars);
|
|
97
|
+
const stats = buildDashboardStats(overriddenDigest, state, filteredMergedPRs.length, filteredClosedPRs.length);
|
|
98
|
+
const dismissedIssues = state.config.dismissedIssues || {};
|
|
99
|
+
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response' && !(i.url in dismissedIssues));
|
|
100
|
+
// Build repo metadata map from repoScores — omit repos without stars or language to avoid empty entries
|
|
101
|
+
const repoMetadata = {};
|
|
102
|
+
for (const [repo, score] of Object.entries(repoScores)) {
|
|
103
|
+
if (score.stargazersCount !== undefined || score.language !== undefined) {
|
|
104
|
+
repoMetadata[repo] = { stars: score.stargazersCount, language: score.language };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// A vetted-list read failure reaches the SPA banner via buildFailures
|
|
108
|
+
// instead of leaving the panel silently empty (#1448).
|
|
109
|
+
const vettedIssues = readVettedIssues(buildFailures);
|
|
110
|
+
if (vettedIssues) {
|
|
111
|
+
stats.availableIssues = vettedIssues.availableCount;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
stats,
|
|
115
|
+
prsByRepo,
|
|
116
|
+
topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
|
|
117
|
+
monthlyMerged,
|
|
118
|
+
monthlyOpened,
|
|
119
|
+
monthlyClosed,
|
|
120
|
+
// #1352: stamp the unified attention bucket so the SPA renders the same
|
|
121
|
+
// taxonomy the CLI brief counts (single classifier, no second opinion).
|
|
122
|
+
// Overrides were already applied when overriddenDigest was built — a
|
|
123
|
+
// second application here would be a no-op but obscures the single
|
|
124
|
+
// apply-then-partition ordering (#1416).
|
|
125
|
+
activePRs: (overriddenDigest.openPRs || []).map((pr) => ({
|
|
126
|
+
...pr,
|
|
127
|
+
attentionBucket: classifyAttentionBucket(pr),
|
|
128
|
+
})),
|
|
129
|
+
// Source of truth is digest.shelvedPRs (union of explicitly-shelved URLs
|
|
130
|
+
// and dormant-non-addressing PRs auto-shelved for display). Returning
|
|
131
|
+
// only state.config.shelvedPRUrls would under-count and desync from
|
|
132
|
+
// stats.shelvedPRs, which is already derived from digest.shelvedPRs. (#981)
|
|
133
|
+
shelvedPRUrls: (overriddenDigest.shelvedPRs || []).map((ref) => ref.url),
|
|
134
|
+
recentlyMergedPRs: overriddenDigest.recentlyMergedPRs || [],
|
|
135
|
+
recentlyClosedPRs: overriddenDigest.recentlyClosedPRs || [],
|
|
136
|
+
autoUnshelvedPRs: overriddenDigest.autoUnshelvedPRs || [],
|
|
137
|
+
commentedIssues,
|
|
138
|
+
issueResponses,
|
|
139
|
+
allMergedPRs: filteredMergedPRs,
|
|
140
|
+
allClosedPRs: filteredClosedPRs,
|
|
141
|
+
repoMetadata,
|
|
142
|
+
vettedIssues,
|
|
143
|
+
// Dedup: the fetch path already applies status overrides into the cached
|
|
144
|
+
// partialFailures, and the rebuild applies them again above — the same
|
|
145
|
+
// failing override must not appear (and be counted) twice (#1448).
|
|
146
|
+
partialFailures: buildFailures.length > 0 ? [...new Set(buildFailures)] : undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export class DashboardCache {
|
|
150
|
+
mergeWarnings;
|
|
151
|
+
// Start immediately with state.json data (written by the daily check that
|
|
152
|
+
// precedes this server launch). A background GitHub fetch refreshes the
|
|
153
|
+
// cache after the port is bound, so the startup poller sees us in time.
|
|
154
|
+
cachedDigest;
|
|
155
|
+
cachedCommentedIssues = [];
|
|
156
|
+
// Persist the last-known partialFailures across rebuild requests (#1035).
|
|
157
|
+
// Cleared only when a fresh fetchDashboardData returns zero failures;
|
|
158
|
+
// re-threaded into every buildDashboardJson call so the SPA banner does
|
|
159
|
+
// not disappear when /api/data rebuilds after a state change or after a
|
|
160
|
+
// POST /api/action completes.
|
|
161
|
+
cachedPartialFailures = undefined;
|
|
162
|
+
cachedJsonData;
|
|
163
|
+
cachedIssueListMtimeMs;
|
|
164
|
+
// Tracks the last background-refresh failure so /api/data can surface
|
|
165
|
+
// staleness to the SPA via the X-Dashboard-Stale header (#1205). Cleared
|
|
166
|
+
// when a refresh succeeds. Without this, token expiry / GitHub outage
|
|
167
|
+
// produces silent stale data hours old with no client-visible signal.
|
|
168
|
+
lastBackgroundRefreshError = null;
|
|
169
|
+
constructor(initialDigest,
|
|
170
|
+
/** Decorates a partialFailures payload with the gist-sync lifecycle's
|
|
171
|
+
* warnings/banners (GistSyncCoordinator.withPendingGistWarnings) on
|
|
172
|
+
* every rebuild — injected so the two lifecycles stay uncoupled. */
|
|
173
|
+
mergeWarnings) {
|
|
174
|
+
this.mergeWarnings = mergeWarnings;
|
|
175
|
+
this.cachedDigest = initialDigest;
|
|
176
|
+
this.cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
177
|
+
}
|
|
178
|
+
get digest() {
|
|
179
|
+
return this.cachedDigest;
|
|
180
|
+
}
|
|
181
|
+
get jsonData() {
|
|
182
|
+
return this.cachedJsonData;
|
|
183
|
+
}
|
|
184
|
+
get issueListMtimeMs() {
|
|
185
|
+
return this.cachedIssueListMtimeMs;
|
|
186
|
+
}
|
|
187
|
+
/** Adopt a full fetchDashboardData result: replace the digest and
|
|
188
|
+
* commented issues unconditionally, and update the persistent banner
|
|
189
|
+
* signal — clear on a clean refresh, set when one or more sub-fetches
|
|
190
|
+
* degraded. See #1035. */
|
|
191
|
+
adoptFetchResult(result) {
|
|
192
|
+
this.cachedDigest = result.digest;
|
|
193
|
+
this.cachedCommentedIssues = result.commentedIssues;
|
|
194
|
+
this.cachedPartialFailures = result.partialFailures.length > 0 ? result.partialFailures : undefined;
|
|
195
|
+
}
|
|
196
|
+
/** A gist pull wholesale-replaces state, and the pulled state.lastDigest
|
|
197
|
+
* may be NEWER than this server's long-lived cachedDigest (another machine
|
|
198
|
+
* ran its daily check). Adopt it when its generatedAt is strictly newer so
|
|
199
|
+
* cross-machine results appear without a manual full refresh (#1446
|
|
200
|
+
* item 6). Unparsable timestamps compare as NaN and never adopt —
|
|
201
|
+
* conservative: keep what we have. */
|
|
202
|
+
adoptNewerPulledDigest(pulled) {
|
|
203
|
+
if (!pulled)
|
|
204
|
+
return;
|
|
205
|
+
if (Date.parse(pulled.generatedAt) > Date.parse(this.cachedDigest.generatedAt)) {
|
|
206
|
+
this.cachedDigest = pulled;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Rebuild the cached JSON payload from the cached digest + the given
|
|
210
|
+
* state, then stamp the issue-list mtime the payload reflects. Throws
|
|
211
|
+
* when buildDashboardJson does — on failure neither the payload nor the
|
|
212
|
+
* mtime is updated, so the next request retries the rebuild. `issueListMtimeMs`
|
|
213
|
+
* lets GET /api/data stamp the value it compared against (#924); the other
|
|
214
|
+
* call sites re-stat after the build. */
|
|
215
|
+
rebuild(state, allMergedPRs, allClosedPRs, issueListMtimeMs) {
|
|
216
|
+
this.cachedJsonData = buildDashboardJson(this.cachedDigest, state, this.cachedCommentedIssues, allMergedPRs, allClosedPRs, this.mergeWarnings(this.cachedPartialFailures));
|
|
217
|
+
this.cachedIssueListMtimeMs = issueListMtimeMs !== undefined ? issueListMtimeMs : getIssueListMtimeMs();
|
|
218
|
+
}
|
|
219
|
+
}
|