Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
92 lines
2.5 KiB
TypeScript
92 lines
2.5 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;
|
|
}
|
|
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|
|
}
|