import type { Db, EventInsert } from './db.js'; import type { DetailPr } from './github/queries.js'; const EXCERPT_LEN = 280; function excerpt(text: string): string { const clean = text.replace(/\s+/g, ' ').trim(); return clean.length > EXCERPT_LEN ? `${clean.slice(0, EXCERPT_LEN - 1)}…` : clean; } const HUNK_LINES = 8; const HUNK_CHARS = 1200; /** GitHub's diffHunk ends at the commented line — keep the trailing context. */ function trimHunk(hunk: string | undefined): string | null { if (!hunk) return null; const lines = hunk.split('\n'); const out = lines.slice(-HUNK_LINES).join('\n'); return out.length > HUNK_CHARS ? out.slice(-HUNK_CHARS) : out; } export interface TimelineOpts { user: string; /** First-ever sync: mark the historical backfill as already read. */ backfillAsRead: boolean; } /** Extract timeline events from a PR detail node and upsert them. * Returns the number of events that were new to the database. */ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number { const events: EventInsert[] = []; for (const c of pr.comments.nodes) { events.push({ id: c.id, prId: pr.id, kind: 'issue_comment', actor: c.author?.login ?? 'ghost', createdAt: c.createdAt, updatedAt: c.updatedAt, bodyExcerpt: excerpt(c.bodyText), url: c.url, reviewState: null, threadId: null, path: null, diffHunk: null, read: false, }); } for (const thread of pr.reviewThreads.nodes) { for (const c of thread.comments.nodes) { events.push({ id: c.id, prId: pr.id, kind: 'review_comment', actor: c.author?.login ?? 'ghost', createdAt: c.createdAt, updatedAt: c.updatedAt, bodyExcerpt: excerpt(c.bodyText), url: c.url, reviewState: null, threadId: thread.id, path: thread.path, diffHunk: trimHunk(c.diffHunk), read: false, }); } } for (const r of pr.reviews.nodes) { if (r.state === 'PENDING') continue; // Empty COMMENTED reviews are wrappers around inline comments — skip to avoid // duplicating the thread items. if (r.state === 'COMMENTED' && r.bodyText.trim() === '') continue; events.push({ id: r.id, prId: pr.id, kind: 'review', actor: r.author?.login ?? 'ghost', createdAt: r.submittedAt ?? '', updatedAt: null, bodyExcerpt: excerpt(r.bodyText), url: r.url, reviewState: r.state, threadId: null, path: null, diffHunk: null, read: false, }); } let newCount = 0; for (const e of events) { if (!e.createdAt) continue; if (db.hasEvent(e.id)) { db.upsertEvent(e); // refresh excerpt/updated_at; read untouched continue; } newCount++; const read = opts.backfillAsRead || e.actor === opts.user; db.upsertEvent({ ...e, read }); } return newCount; }