export function unionIntervals(intervals) { const sorted = intervals.filter(([start, end]) => end > start).sort((a, b) => a[0] - b[0] || a[1] - b[1]); const result = []; for (const [start, end] of sorted) { const last = result.at(-1); if (last && start <= last[1]) last[1] = Math.max(last[1], end); else result.push([start, end]); } return result; } export function intersectIntervals(left, right) { const a = unionIntervals(left), b = unionIntervals(right), result = []; let i = 0, j = 0; while (i < a.length && j < b.length) { const start = Math.max(a[i][0], b[j][0]), end = Math.min(a[i][1], b[j][1]); if (end > start) result.push([start, end]); if (a[i][1] <= b[j][1]) i++; else j++; } return result; } export function subtractIntervals(intervals, excluded) { const result = [], exclusions = unionIntervals(excluded); for (const [start, end] of unionIntervals(intervals)) { let cursor = start; for (const [a, b] of exclusions) { if (b <= cursor) continue; if (a >= end) break; if (a > cursor) result.push([cursor, Math.min(a, end)]); cursor = Math.max(cursor, b); if (cursor >= end) break; } if (cursor < end) result.push([cursor, end]); } return result; } export const intervalDuration = intervals => unionIntervals(intervals).reduce((sum, [start, end]) => sum + end - start, 0); export function reliabilitySummary({ availability, scenario, incidents, maintenance, since, now, lastRecovery }) { const excluded = unionIntervals(maintenance); const observed = subtractIntervals(scenario ? intersectIntervals(availability.known, scenario.known) : availability.known, excluded); const outages = []; let incomplete = false; for (const incident of incidents) { const expected = subtractIntervals([[Math.max(since, incident.startedAt), Math.min(now, incident.resolvedAt ?? now)]], excluded); if (!expected.length) continue; const data = incident.scope === 'scenario' ? scenario : availability; const measured = intersectIntervals(expected, data?.down || []); if (intervalDuration(intersectIntervals(expected, data?.known || [])) < intervalDuration(expected)) incomplete = true; outages.push({ intervals: measured, duration: intervalDuration(measured) }); } const measured = unionIntervals(outages.flatMap(item => item.intervals)); const duration = intervalDuration(measured); const hasData = observed.length > 0 || duration > 0; const usable = hasData && !(incomplete && duration === 0); return { downtimeMs: usable ? duration : null, longestDowntimeMs: usable ? outages.reduce((longest, item) => Math.max(longest, item.duration), 0) : null, incidentCount: outages.length, downtimeIncomplete: incomplete, lastRecoveryAt: lastRecovery?.time ?? null, lastRecoveryScope: lastRecovery?.scope ?? null, observedMs: intervalDuration(observed), coverage: now > since ? Number((intervalDuration(observed) / (now - since) * 100).toFixed(4)) : 0, since, asOf: now, }; }