From 992c88171f871e0ca5913b62f1c27dc20f3f8398 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sat, 15 Aug 2026 00:02:11 +0530 Subject: [PATCH] chore: reduce code-health baseline debt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split buildWeeklyDigest (CCN 36) in src/lib/digest.ts into focused helper functions: resolveSender, updateSenderMap, updateThreadMap, updateThemeCounts, processEmail, collectRelationshipsQuiet, collectThreadsToRevisit, collectWeeklyThemes. Also moved parseSender and domainFromEmail to end of file to fix a lizard parser false positive that inflated the reported function length. Behavior is preserved — all existing unit tests and the golden-hash digest-performance test pass unchanged. Lowers the ratcheted complexity baseline in scripts/check-code-health.mjs: - violations: 16 → 14 - maxCcn: 36 → 32 Progress on #32 --- scripts/check-code-health.mjs | 2 +- src/lib/digest.ts | 293 ++++++++++++++++++++++------------ 2 files changed, 188 insertions(+), 107 deletions(-) diff --git a/scripts/check-code-health.mjs b/scripts/check-code-health.mjs index cd5b866..2cd2cb7 100644 --- a/scripts/check-code-health.mjs +++ b/scripts/check-code-health.mjs @@ -52,7 +52,7 @@ function checkComplexity() { maxParams: Math.max(...rows.map((row) => row[3])), }; // Ratcheted legacy debt: https://github.com/Significant-Hobbies/email-manager/issues/32 - const baseline = { violations: 16, maxCcn: 36, maxLength: 294, maxParams: 11 }; + const baseline = { violations: 14, maxCcn: 32, maxLength: 294, maxParams: 11 }; const regressions = Object.entries(baseline).filter(([key, maximum]) => observed[key] > maximum); console.log( `Complexity: ${observed.functions} functions, ${observed.nloc} NLOC, ` + diff --git a/src/lib/digest.ts b/src/lib/digest.ts index f3c5c0c..081d821 100644 --- a/src/lib/digest.ts +++ b/src/lib/digest.ts @@ -62,18 +62,6 @@ export interface BuildWeeklyDigestOptions { const NEWSLETTER_RE = /\b(newsletter|digest|weekly|roundup|webinar|promotion|sale|unsubscribe|noreply|no-reply)\b/i; -function parseSender(from: string): { email: string; displayName: string } { - const emailMatch = from.match(/<([^>]+)>/); - const email = (emailMatch?.[1] ?? from).toLowerCase().trim(); - const displayName = from.replace(/<[^>]+>/, '').trim() || email; - return { email, displayName }; -} - -function domainFromEmail(email: string): string { - const m = email.match(/@(.+)/); - return m?.[1] ?? 'unknown'; -} - function startOfUtcWeek(d: Date): Date { const copy = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); const day = copy.getUTCDay(); @@ -95,108 +83,129 @@ function getCachedTimestamp(value: string, cache: Map): number { return parsed; } -/** - * Build a deterministic weekly digest from locally cached emails. - */ -export function buildWeeklyDigest( - emails: DigestEmailInput[], - options: BuildWeeklyDigestOptions = {} -): WeeklyDigest { - const now = options.now ?? new Date(); - const quietWindowDays = options.quietWindowDays ?? 60; - const minPriorMessages = options.minPriorMessages ?? 3; - const staleThreadDays = options.staleThreadDays ?? 14; - - const periodEnd = startOfUtcWeek(now); - const periodStart = new Date(periodEnd); - periodStart.setUTCDate(periodStart.getUTCDate() - 7); - - const quietCutoff = new Date(now); - quietCutoff.setUTCDate(quietCutoff.getUTCDate() - quietWindowDays); - - const staleCutoff = new Date(now); - staleCutoff.setUTCDate(staleCutoff.getUTCDate() - staleThreadDays); +interface SenderAccum { + displayName: string; + lastAt: number; + count: number; +} - const weekStartMs = periodStart.getTime(); - const weekEndMs = periodEnd.getTime() + 7 * 24 * 60 * 60 * 1000; - const quietCutoffMs = quietCutoff.getTime(); - const staleCutoffMs = staleCutoff.getTime(); - const nowMs = now.getTime(); +interface ThreadAccum { + subject: string; + lastAt: number; + messageCount: number; + starred: boolean; +} - const senderMap = new Map(); +interface SenderDetails { + email: string; + displayName: string; + newsletter: boolean; + domain: string; +} - const threadMap = new Map< - string, - { - subject: string; - lastAt: number; - messageCount: number; - starred: boolean; - } - >(); +interface Accumulators { + senderMap: Map; + threadMap: Map; + themeDomainCounts: Map>; + senderCache: Map; + dateCache: Map; + themeKeyCache: Map; +} - const themeDomainCounts = new Map>(); - const senderCache = new Map< - string, - { email: string; displayName: string; newsletter: boolean; domain: string } - >(); - const dateCache = new Map(); - const themeKeyCache = new Map(); +function resolveSender(from: string, senderCache: Map): SenderDetails { + let senderDetails = senderCache.get(from); + if (!senderDetails) { + const parsed = parseSender(from); + senderDetails = { + ...parsed, + newsletter: isNewsletterSender(parsed.email, parsed.displayName), + domain: domainFromEmail(parsed.email), + }; + senderCache.set(from, senderDetails); + } + return senderDetails; +} - for (const email of emails) { - let senderDetails = senderCache.get(email.from); - if (!senderDetails) { - const parsed = parseSender(email.from); - senderDetails = { - ...parsed, - newsletter: isNewsletterSender(parsed.email, parsed.displayName), - domain: domainFromEmail(parsed.email), - }; - senderCache.set(email.from, senderDetails); - } - if (senderDetails.newsletter) continue; - - const t = getCachedTimestamp(email.date, dateCache); - if (Number.isNaN(t)) continue; - - const sender = senderMap.get(senderDetails.email); - if (sender) { - if (t > sender.lastAt) sender.lastAt = t; - sender.count += 1; - } else { - senderMap.set(senderDetails.email, { - displayName: senderDetails.displayName, - lastAt: t, - count: 1, - }); - } +function updateSenderMap( + senderMap: Map, + email: string, + displayName: string, + t: number +): void { + const sender = senderMap.get(email); + if (sender) { + if (t > sender.lastAt) sender.lastAt = t; + sender.count += 1; + } else { + senderMap.set(email, { displayName, lastAt: t, count: 1 }); + } +} - const thread = threadMap.get(email.threadId); - if (thread) { - if (t > thread.lastAt) thread.lastAt = t; - thread.messageCount += 1; - if (email.labelIds?.includes('STARRED')) thread.starred = true; - } else { - threadMap.set(email.threadId, { - subject: email.subject, - lastAt: t, - messageCount: 1, - starred: email.labelIds?.includes('STARRED') ?? false, - }); - } +function updateThreadMap( + threadMap: Map, + threadId: string, + subject: string, + t: number, + labelIds: string[] | undefined +): void { + const thread = threadMap.get(threadId); + if (thread) { + if (t > thread.lastAt) thread.lastAt = t; + thread.messageCount += 1; + if (labelIds?.includes('STARRED')) thread.starred = true; + } else { + threadMap.set(threadId, { + subject, + lastAt: t, + messageCount: 1, + starred: labelIds?.includes('STARRED') ?? false, + }); + } +} - if (t >= weekStartMs && t < weekEndMs) { - let themeKey = themeKeyCache.get(email.subject); - if (!themeKey) { - themeKey = themeKeyFromSubject(email.subject); - themeKeyCache.set(email.subject, themeKey); - } - const domainMap = themeDomainCounts.get(themeKey) ?? new Map(); - domainMap.set(senderDetails.domain, (domainMap.get(senderDetails.domain) ?? 0) + 1); - themeDomainCounts.set(themeKey, domainMap); - } +function updateThemeCounts( + email: DigestEmailInput, + senderDetails: SenderDetails, + acc: Accumulators, + t: number, + weekStartMs: number, + weekEndMs: number +): void { + if (t < weekStartMs || t >= weekEndMs) return; + + let themeKey = acc.themeKeyCache.get(email.subject); + if (!themeKey) { + themeKey = themeKeyFromSubject(email.subject); + acc.themeKeyCache.set(email.subject, themeKey); } + const domainMap = acc.themeDomainCounts.get(themeKey) ?? new Map(); + domainMap.set(senderDetails.domain, (domainMap.get(senderDetails.domain) ?? 0) + 1); + acc.themeDomainCounts.set(themeKey, domainMap); +} + +function processEmail( + email: DigestEmailInput, + acc: Accumulators, + weekStartMs: number, + weekEndMs: number +): void { + const senderDetails = resolveSender(email.from, acc.senderCache); + if (senderDetails.newsletter) return; + + const t = getCachedTimestamp(email.date, acc.dateCache); + if (Number.isNaN(t)) return; + + updateSenderMap(acc.senderMap, senderDetails.email, senderDetails.displayName, t); + updateThreadMap(acc.threadMap, email.threadId, email.subject, t, email.labelIds); + updateThemeCounts(email, senderDetails, acc, t, weekStartMs, weekEndMs); +} +function collectRelationshipsQuiet( + senderMap: Map, + minPriorMessages: number, + quietCutoffMs: number, + nowMs: number +): RelationshipQuiet[] { const relationshipsQuiet: RelationshipQuiet[] = []; for (const [senderEmail, data] of senderMap) { if (data.count < minPriorMessages) continue; @@ -212,7 +221,13 @@ export function buildWeeklyDigest( }); } relationshipsQuiet.sort((a, b) => b.priorMessageCount - a.priorMessageCount); + return relationshipsQuiet; +} +function collectThreadsToRevisit( + threadMap: Map, + staleCutoffMs: number +): ThreadRevisit[] { const threadsToRevisit: ThreadRevisit[] = []; for (const [threadId, data] of threadMap) { if (data.lastAt >= staleCutoffMs) continue; @@ -241,8 +256,11 @@ export function buildWeeklyDigest( threadsToRevisit.sort((a, b) => a.lastMessageAt < b.lastMessageAt ? -1 : a.lastMessageAt > b.lastMessageAt ? 1 : 0 ); + return threadsToRevisit; +} - const weeklyThemes: WeeklyTheme[] = [...themeDomainCounts.entries()] +function collectWeeklyThemes(themeDomainCounts: Map>): WeeklyTheme[] { + return [...themeDomainCounts.entries()] .map(([id, domainMap]) => { const entries = [...domainMap.entries()].sort((a, b) => b[1] - a[1]); const messageCount = entries.reduce((s, [, n]) => s + n, 0); @@ -255,6 +273,57 @@ export function buildWeeklyDigest( }) .sort((a, b) => b.messageCount - a.messageCount) .slice(0, 5); +} + +/** + * Build a deterministic weekly digest from locally cached emails. + */ +export function buildWeeklyDigest( + emails: DigestEmailInput[], + options: BuildWeeklyDigestOptions = {} +): WeeklyDigest { + const now = options.now ?? new Date(); + const quietWindowDays = options.quietWindowDays ?? 60; + const minPriorMessages = options.minPriorMessages ?? 3; + const staleThreadDays = options.staleThreadDays ?? 14; + + const periodEnd = startOfUtcWeek(now); + const periodStart = new Date(periodEnd); + periodStart.setUTCDate(periodStart.getUTCDate() - 7); + + const quietCutoff = new Date(now); + quietCutoff.setUTCDate(quietCutoff.getUTCDate() - quietWindowDays); + + const staleCutoff = new Date(now); + staleCutoff.setUTCDate(staleCutoff.getUTCDate() - staleThreadDays); + + const weekStartMs = periodStart.getTime(); + const weekEndMs = periodEnd.getTime() + 7 * 24 * 60 * 60 * 1000; + const quietCutoffMs = quietCutoff.getTime(); + const staleCutoffMs = staleCutoff.getTime(); + const nowMs = now.getTime(); + + const acc: Accumulators = { + senderMap: new Map(), + threadMap: new Map(), + themeDomainCounts: new Map>(), + senderCache: new Map(), + dateCache: new Map(), + themeKeyCache: new Map(), + }; + + for (const email of emails) { + processEmail(email, acc, weekStartMs, weekEndMs); + } + + const relationshipsQuiet = collectRelationshipsQuiet( + acc.senderMap, + minPriorMessages, + quietCutoffMs, + nowMs + ); + const threadsToRevisit = collectThreadsToRevisit(acc.threadMap, staleCutoffMs); + const weeklyThemes = collectWeeklyThemes(acc.themeDomainCounts); return { format: DIGEST_FORMAT, @@ -316,3 +385,15 @@ export function digestToTodayLittleLogExport(digest: WeeklyDigest): { ], }; } + +function parseSender(from: string): { email: string; displayName: string } { + const emailMatch = from.match(/<([^>]+)>/); + const email = (emailMatch?.[1] ?? from).toLowerCase().trim(); + const displayName = from.replace(/<[^>]+>/, '').trim() || email; + return { email, displayName }; +} + +function domainFromEmail(email: string): string { + const m = email.match(/@(.+)/); + return m ? m[1] : 'unknown'; +}