import { repoHtmlUrl } from './repo.ts' import { getRepoSettings, ingestSummary, isFileKind, listCommentaries, loadCommitRows, } from './storage.ts' import type { ChartMode, FileKind, SeriesPayload, SeriesPoint } from './types.ts' /** * Build the commit-level series used by the hosted chart. * Call when rendering the app or when an agent needs the same numbers the UI graphs. * * @param input - Optional time bounds and chart mode * @returns Points, languages, kinds, and commentaries for the selected window * * @example * import getSeries from 'kody:@kentcdodds/lineage/series' * * const series = await getSeries({ mode: 'cumulative' }) */ export default async function getSeries( input: { from?: string | null to?: string | null mode?: ChartMode } = {}, ): Promise { const repo = await getRepoSettings() const mode: ChartMode = input.mode ?? 'cumulative' const { commits, buckets } = await loadCommitRows(input.to) const bucketsBySha = new Map< string, Array<{ language: string; kind: FileKind; additions: number; deletions: number }> >() const languages = new Set() const kinds = new Set() for (const row of buckets) { if ( typeof row.sha !== 'string' || typeof row.language !== 'string' || typeof row.kind !== 'string' || !isFileKind(row.kind) ) { continue } const additions = Number(row.additions ?? 0) const deletions = Number(row.deletions ?? 0) const list = bucketsBySha.get(row.sha) ?? [] list.push({ language: row.language, kind: row.kind, additions, deletions }) bucketsBySha.set(row.sha, list) languages.add(row.language) kinds.add(row.kind) } const running = new Map() const points: SeriesPoint[] = [] for (const row of commits) { if ( typeof row.sha !== 'string' || typeof row.committed_at !== 'string' || typeof row.message !== 'string' || typeof row.html_url !== 'string' ) { continue } const commitBuckets = bucketsBySha.get(row.sha) ?? [] let additions = 0 let deletions = 0 const serialized = commitBuckets.map((item) => { const key = `${item.language}\t${item.kind}` const net = item.additions - item.deletions const next = (running.get(key) ?? 0) + net running.set(key, next) additions += item.additions deletions += item.deletions return { language: item.language, kind: item.kind, additions: item.additions, deletions: item.deletions, net, cumulative: next, } }) let cumulative = 0 for (const value of running.values()) cumulative += value if (input.from && row.committed_at < input.from) continue points.push({ sha: row.sha, at: row.committed_at, message: row.message.split('\n')[0] ?? row.message, htmlUrl: row.html_url, additions, deletions, net: additions - deletions, cumulative, buckets: serialized, }) } const commentaries = (await listCommentaries()).filter((item) => points.some((point) => point.sha === item.sha), ) return { repo: { ...repo, htmlUrl: repoHtmlUrl(repo) }, mode, from: input.from ?? null, to: input.to ?? null, generatedAt: new Date().toISOString(), commitCount: points.length, languages: [...languages].sort(), kinds: (['source', 'tests', 'docs'] as FileKind[]).filter((kind) => kinds.has(kind), ), points, commentaries, ingest: await ingestSummary(), } }