(runs: MaintainRun[])
| 25 | } |
| 26 | |
| 27 | export function buildMaintainStats(runs: MaintainRun[]): MaintainStats { |
| 28 | const opened = runs.filter(r => r.status === 'opened').length; |
| 29 | const blocked = runs.filter(r => r.status === 'blocked').length; |
| 30 | const failed = runs.filter(r => r.status === 'failed' || r.status === 'error').length; |
| 31 | const filesCleaned = runs.filter(r => r.status === 'opened').reduce((a, r) => a + (r.filesChanged || 0), 0); |
| 32 | |
| 33 | const byScopeMap = new Map<string, { runs: number; opened: number }>(); |
| 34 | for (const r of runs) { |
| 35 | const e = byScopeMap.get(r.scope) ?? { runs: 0, opened: 0 }; |
| 36 | e.runs++; if (r.status === 'opened') e.opened++; |
| 37 | byScopeMap.set(r.scope, e); |
| 38 | } |
| 39 | const byScope = [...byScopeMap.entries()] |
| 40 | .map(([scope, v]) => ({ scope, ...v })) |
| 41 | .sort((a, b) => b.runs - a.runs); |
| 42 | |
| 43 | return { |
| 44 | totalRuns: runs.length, |
| 45 | opened, blocked, failed, |
| 46 | successRate: runs.length ? opened / runs.length : 0, |
| 47 | filesCleaned, |
| 48 | estMinutesSaved: opened * 5, |
| 49 | byScope, |
| 50 | lastRun: runs[0] ? { when: runs[0].when, status: runs[0].status, scope: runs[0].scope } : undefined, |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | /** Suggest scopes that haven't run yet (so the dashboard can nudge what to schedule next). PURE. */ |
| 55 | export function suggestNextScopes(stats: MaintainStats, allScopes: readonly string[]): string[] { |
no test coverage detected