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<SeriesPayload> {
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<string>()
const kinds = new Set<FileKind>()
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<string, number>()
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(),
}
}