125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
import { z } from 'zod';
|
|
import type { EcosystemEvent } from './ecosystem.js';
|
|
|
|
export const TimelineVisibilitySchema = z.enum(['private', 'org', 'shared', 'local-only']);
|
|
|
|
export const TimelineItemSchema = z.object({
|
|
itemId: z.string().min(1),
|
|
occurredAt: z.string().datetime(),
|
|
eventName: z.string().min(1),
|
|
productId: z.string().min(1),
|
|
title: z.string().min(1),
|
|
summary: z.string().nullable().optional(),
|
|
artifactRefs: z.array(z.string().min(1)),
|
|
relatedEventIds: z.array(z.string().min(1)),
|
|
actorType: z.enum(['user', 'agent', 'system', 'device']),
|
|
visibility: TimelineVisibilitySchema,
|
|
correlationId: z.string().min(1).nullable().optional(),
|
|
});
|
|
|
|
export type TimelineItem = z.infer<typeof TimelineItemSchema>;
|
|
|
|
type EventLike = Pick<
|
|
EcosystemEvent,
|
|
| 'eventId'
|
|
| 'eventName'
|
|
| 'occurredAt'
|
|
| 'productId'
|
|
| 'artifactId'
|
|
| 'actor'
|
|
| 'trace'
|
|
| 'payload'
|
|
>;
|
|
|
|
function titleForEvent(event: EventLike): string {
|
|
if (event.eventName === 'capture.transcript.created') {
|
|
return 'Transcript captured';
|
|
}
|
|
if (event.eventName === 'memory.entry.created') {
|
|
const kind = typeof event.payload.memoryKind === 'string' ? event.payload.memoryKind : 'memory';
|
|
return `${capitalize(kind)} memory proposed`;
|
|
}
|
|
if (event.eventName === 'artifact.created') {
|
|
const type =
|
|
typeof event.payload.artifactType === 'string' ? event.payload.artifactType : 'artifact';
|
|
const title =
|
|
typeof event.payload.title === 'string' && event.payload.title ? event.payload.title : null;
|
|
return title ? title : `${capitalize(type)} created`;
|
|
}
|
|
if (event.eventName === 'artifact.linked') {
|
|
const relation = typeof event.payload.relation === 'string' ? event.payload.relation : 'linked';
|
|
return `Artifact ${relation}`;
|
|
}
|
|
return event.eventName;
|
|
}
|
|
|
|
function summaryForEvent(event: EventLike): string | null {
|
|
if (event.eventName === 'capture.transcript.created') {
|
|
const duration =
|
|
typeof event.payload.durationMs === 'number'
|
|
? `${event.payload.durationMs}ms`
|
|
: 'unknown duration';
|
|
const source =
|
|
typeof event.payload.transcriptSource === 'string'
|
|
? event.payload.transcriptSource
|
|
: 'unknown source';
|
|
return `${source} transcript captured (${duration})`;
|
|
}
|
|
if (event.eventName === 'memory.entry.created') {
|
|
const sources = Array.isArray(event.payload.sourceArtifactIds)
|
|
? event.payload.sourceArtifactIds.length
|
|
: 0;
|
|
return `${sources} source artifact${sources === 1 ? '' : 's'} linked`;
|
|
}
|
|
if (event.eventName === 'artifact.created') {
|
|
const type =
|
|
typeof event.payload.artifactType === 'string' ? event.payload.artifactType : 'artifact';
|
|
const status = typeof event.payload.status === 'string' ? event.payload.status : 'created';
|
|
return `${type} status: ${status}`;
|
|
}
|
|
if (event.eventName === 'artifact.linked') {
|
|
const target =
|
|
typeof event.payload.targetArtifactId === 'string'
|
|
? event.payload.targetArtifactId
|
|
: 'unknown target';
|
|
return `Linked to ${target}`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function relatedEventIdsForEvent(event: EventLike): string[] {
|
|
return Array.from(
|
|
new Set(
|
|
[event.trace.parentEventId, event.trace.causationId].filter(
|
|
(value): value is string => typeof value === 'string' && value.length > 0
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
export function buildTimelineItem(event: EventLike): TimelineItem {
|
|
return TimelineItemSchema.parse({
|
|
itemId: `timeline_${event.eventId}`,
|
|
occurredAt: event.occurredAt,
|
|
eventName: event.eventName,
|
|
productId: event.productId,
|
|
title: titleForEvent(event),
|
|
summary: summaryForEvent(event),
|
|
artifactRefs: [event.artifactId].filter((value): value is string => Boolean(value)),
|
|
relatedEventIds: relatedEventIdsForEvent(event),
|
|
actorType: event.actor.actorType,
|
|
visibility: 'private',
|
|
correlationId: event.trace.correlationId ?? null,
|
|
});
|
|
}
|
|
|
|
export function buildTimelineItems(events: EventLike[]): TimelineItem[] {
|
|
return events
|
|
.map(buildTimelineItem)
|
|
.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt));
|
|
}
|
|
|
|
function capitalize(value: string): string {
|
|
return value.length === 0 ? value : `${value[0]!.toUpperCase()}${value.slice(1)}`;
|
|
}
|