pr-monitor/server/timeline.ts
2026-07-28 14:51:01 +00:00

106 lines
2.9 KiB
TypeScript

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;
}