feat(notes): import phase1 transcript artifacts
This commit is contained in:
parent
59d13e423e
commit
6ffc2f8755
94
backend/src/lib/ecosystem-phase1.test.ts
Normal file
94
backend/src/lib/ecosystem-phase1.test.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
buildPhase1NoteImport,
|
||||
loadLatestTranscriptArtifact,
|
||||
persistPhase1NoteOutputs,
|
||||
} from './ecosystem-phase1.js';
|
||||
|
||||
describe('ecosystem phase1 note import', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.BYTELYST_ECOSYSTEM_DIR;
|
||||
});
|
||||
|
||||
it('loads the latest transcript and persists a linked note artifact + events', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'notelett-phase1-'));
|
||||
process.env.BYTELYST_ECOSYSTEM_DIR = root;
|
||||
|
||||
const transcriptArtifact = {
|
||||
id: 'art_transcript_123',
|
||||
title: 'Standup capture',
|
||||
summary: 'Transcript summary',
|
||||
sourceSurface: 'desktop',
|
||||
createdAt: '2026-04-03T18:15:00.000Z',
|
||||
tags: ['voice'],
|
||||
ownership: { userId: 'user_saravana', orgId: null },
|
||||
provenance: {
|
||||
originProductId: 'lysnrai',
|
||||
originActionId: 'capture_123',
|
||||
sessionId: 'sess_123',
|
||||
runId: null,
|
||||
approvalId: null,
|
||||
correlationId: 'corr_123',
|
||||
lineage: [
|
||||
{
|
||||
stepType: 'captured',
|
||||
productId: 'lysnrai',
|
||||
actorType: 'user',
|
||||
timestamp: '2026-04-03T18:15:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
payload: {
|
||||
transcriptText: 'Transcript body',
|
||||
transcriptSource: 'microphone',
|
||||
language: 'en',
|
||||
durationMs: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
await mkdir(join(root, 'indexes'), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, 'indexes', 'latest-transcript.json'),
|
||||
`${JSON.stringify(transcriptArtifact, null, 2)}\n`,
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const loaded = await loadLatestTranscriptArtifact(root);
|
||||
const generated = buildPhase1NoteImport({
|
||||
transcriptArtifact: loaded,
|
||||
workspaceId: 'ws_phase1',
|
||||
userId: 'user_saravana',
|
||||
now: '2026-04-03T18:17:00.000Z',
|
||||
});
|
||||
|
||||
await persistPhase1NoteOutputs({
|
||||
ecosystemNoteArtifact: generated.ecosystemNoteArtifact,
|
||||
createdEvent: generated.createdEvent,
|
||||
linkedEvent: generated.linkedEvent,
|
||||
root,
|
||||
});
|
||||
|
||||
const savedNoteArtifact = JSON.parse(
|
||||
await readFile(join(root, 'artifacts', 'note', `${generated.ecosystemNoteArtifact.id}.json`), 'utf-8')
|
||||
);
|
||||
const savedLinkedEvent = JSON.parse(
|
||||
await readFile(join(root, 'events', 'artifact.linked', `${generated.linkedEvent.eventId}.json`), 'utf-8')
|
||||
);
|
||||
|
||||
expect(generated.note.sourceUri).toBe('art_transcript_123');
|
||||
expect(generated.ecosystemNoteArtifact.links).toEqual([
|
||||
{
|
||||
relation: 'summarizes',
|
||||
targetArtifactId: 'art_transcript_123',
|
||||
},
|
||||
]);
|
||||
expect(savedNoteArtifact.payload.noteFormat).toBe('markdown');
|
||||
expect(savedLinkedEvent.payload.targetArtifactId).toBe('art_transcript_123');
|
||||
expect(savedLinkedEvent.payload.relation).toBe('summarizes');
|
||||
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
293
backend/src/lib/ecosystem-phase1.ts
Normal file
293
backend/src/lib/ecosystem-phase1.ts
Normal file
@ -0,0 +1,293 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import type { NoteArtifactDoc } from '../modules/note-artifacts/types.js';
|
||||
import type { NoteDoc } from '../modules/notes/types.js';
|
||||
|
||||
export const DEFAULT_PHASE1_ROOT = join(homedir(), '.bytelyst', 'ecosystem', 'phase1');
|
||||
|
||||
type TranscriptArtifact = {
|
||||
id: string;
|
||||
title: string | null;
|
||||
summary: string | null;
|
||||
sourceSurface: string;
|
||||
createdAt: string;
|
||||
tags: string[];
|
||||
ownership: {
|
||||
userId: string;
|
||||
orgId?: string | null;
|
||||
};
|
||||
provenance: {
|
||||
originProductId: string;
|
||||
originActionId?: string | null;
|
||||
sessionId?: string | null;
|
||||
runId?: string | null;
|
||||
approvalId?: string | null;
|
||||
correlationId?: string | null;
|
||||
lineage: Array<{
|
||||
stepType: string;
|
||||
productId: string;
|
||||
actorType: 'user' | 'agent' | 'system';
|
||||
timestamp: string;
|
||||
}>;
|
||||
};
|
||||
payload: {
|
||||
transcriptText: string;
|
||||
transcriptSource: string;
|
||||
language: string;
|
||||
durationMs: number;
|
||||
};
|
||||
};
|
||||
|
||||
type EcosystemEvent = {
|
||||
eventId: string;
|
||||
eventName: string;
|
||||
eventVersion: 1;
|
||||
occurredAt: string;
|
||||
productId: string;
|
||||
sourceSurface: string;
|
||||
userId: string | null;
|
||||
orgId?: string | null;
|
||||
sessionId?: string | null;
|
||||
runId?: string | null;
|
||||
artifactId?: string | null;
|
||||
actor: {
|
||||
actorType: 'user' | 'agent' | 'system' | 'device';
|
||||
actorId?: string | null;
|
||||
};
|
||||
trace: {
|
||||
correlationId: string | null;
|
||||
causationId: string | null;
|
||||
parentEventId: string | null;
|
||||
};
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type NoteArtifactEnvelope = {
|
||||
id: string;
|
||||
artifactType: 'note';
|
||||
schemaVersion: 1;
|
||||
productId: 'notelett';
|
||||
sourceSurface: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: {
|
||||
actorType: 'agent';
|
||||
actorId: string;
|
||||
};
|
||||
ownership: {
|
||||
userId: string;
|
||||
orgId?: string | null;
|
||||
};
|
||||
visibility: {
|
||||
scope: 'private';
|
||||
};
|
||||
status: string;
|
||||
tags: string[];
|
||||
links: Array<{
|
||||
relation: 'summarizes';
|
||||
targetArtifactId: string;
|
||||
}>;
|
||||
provenance: TranscriptArtifact['provenance'];
|
||||
payload: {
|
||||
noteFormat: 'markdown';
|
||||
body: string;
|
||||
excerpt: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function getPhase1Root(): string {
|
||||
return process.env.BYTELYST_ECOSYSTEM_DIR ?? DEFAULT_PHASE1_ROOT;
|
||||
}
|
||||
|
||||
export async function loadLatestTranscriptArtifact(root = getPhase1Root()): Promise<TranscriptArtifact> {
|
||||
const raw = await readFile(join(root, 'indexes', 'latest-transcript.json'), 'utf-8');
|
||||
return JSON.parse(raw) as TranscriptArtifact;
|
||||
}
|
||||
|
||||
export function buildPhase1NoteImport(params: {
|
||||
transcriptArtifact: TranscriptArtifact;
|
||||
workspaceId: string;
|
||||
userId: string;
|
||||
now?: string;
|
||||
}): {
|
||||
note: NoteDoc;
|
||||
noteArtifactDoc: NoteArtifactDoc;
|
||||
ecosystemNoteArtifact: NoteArtifactEnvelope;
|
||||
createdEvent: EcosystemEvent;
|
||||
linkedEvent: EcosystemEvent;
|
||||
} {
|
||||
const { transcriptArtifact, workspaceId, userId } = params;
|
||||
const now = params.now ?? new Date().toISOString();
|
||||
const noteId = `note_phase1_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
||||
const correlationId = transcriptArtifact.provenance.correlationId ?? `corr_${randomUUID().slice(0, 12)}`;
|
||||
const runId = `run_note_${randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
||||
const title = transcriptArtifact.title || 'Imported transcript note';
|
||||
const body = `# ${title}\n\n${transcriptArtifact.payload.transcriptText}`;
|
||||
const excerpt = transcriptArtifact.summary || transcriptArtifact.payload.transcriptText.slice(0, 160);
|
||||
const noteArtifactId = `art_note_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
||||
|
||||
const note: NoteDoc = {
|
||||
id: noteId,
|
||||
productId: 'notelett',
|
||||
workspaceId,
|
||||
userId,
|
||||
title,
|
||||
body,
|
||||
status: 'draft',
|
||||
tags: Array.from(new Set([...transcriptArtifact.tags, 'ecosystem', 'phase1'])),
|
||||
links: [transcriptArtifact.id],
|
||||
sourceType: 'ecosystem-transcript',
|
||||
sourceUri: transcriptArtifact.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: userId,
|
||||
updatedBy: userId,
|
||||
agentId: 'phase1-transcript-importer',
|
||||
};
|
||||
|
||||
const noteArtifactDoc: NoteArtifactDoc = {
|
||||
id: `na_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
||||
productId: 'notelett',
|
||||
workspaceId,
|
||||
userId,
|
||||
noteId,
|
||||
artifactType: 'summary',
|
||||
title: 'Imported transcript artifact',
|
||||
description: `Transcript source ${transcriptArtifact.id}`,
|
||||
blobPath: join(getPhase1Root(), 'artifacts', 'note', `${noteArtifactId}.json`),
|
||||
contentType: 'application/json',
|
||||
createdAt: now,
|
||||
createdBy: userId,
|
||||
updatedAt: now,
|
||||
updatedBy: userId,
|
||||
};
|
||||
|
||||
const ecosystemNoteArtifact: NoteArtifactEnvelope = {
|
||||
id: noteArtifactId,
|
||||
artifactType: 'note',
|
||||
schemaVersion: 1,
|
||||
productId: 'notelett',
|
||||
sourceSurface: 'backend',
|
||||
title,
|
||||
summary: excerpt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: {
|
||||
actorType: 'agent',
|
||||
actorId: 'phase1-transcript-importer',
|
||||
},
|
||||
ownership: {
|
||||
userId,
|
||||
orgId: transcriptArtifact.ownership.orgId ?? null,
|
||||
},
|
||||
visibility: {
|
||||
scope: 'private',
|
||||
},
|
||||
status: note.status,
|
||||
tags: note.tags,
|
||||
links: [
|
||||
{
|
||||
relation: 'summarizes',
|
||||
targetArtifactId: transcriptArtifact.id,
|
||||
},
|
||||
],
|
||||
provenance: {
|
||||
...transcriptArtifact.provenance,
|
||||
runId,
|
||||
lineage: [
|
||||
...transcriptArtifact.provenance.lineage,
|
||||
{
|
||||
stepType: 'note-created',
|
||||
productId: 'notelett',
|
||||
actorType: 'agent',
|
||||
timestamp: now,
|
||||
},
|
||||
],
|
||||
},
|
||||
payload: {
|
||||
noteFormat: 'markdown',
|
||||
body,
|
||||
excerpt,
|
||||
},
|
||||
};
|
||||
|
||||
const createdEvent: EcosystemEvent = {
|
||||
eventId: `evt_note_created_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
||||
eventName: 'artifact.created',
|
||||
eventVersion: 1,
|
||||
occurredAt: now,
|
||||
productId: 'notelett',
|
||||
sourceSurface: 'backend',
|
||||
userId,
|
||||
orgId: transcriptArtifact.ownership.orgId ?? null,
|
||||
sessionId: transcriptArtifact.provenance.sessionId ?? null,
|
||||
runId,
|
||||
artifactId: ecosystemNoteArtifact.id,
|
||||
actor: {
|
||||
actorType: 'agent',
|
||||
actorId: 'phase1-transcript-importer',
|
||||
},
|
||||
trace: {
|
||||
correlationId,
|
||||
causationId: null,
|
||||
parentEventId: null,
|
||||
},
|
||||
payload: {
|
||||
artifactType: 'note',
|
||||
title,
|
||||
status: note.status,
|
||||
},
|
||||
};
|
||||
|
||||
const linkedEvent: EcosystemEvent = {
|
||||
eventId: `evt_note_linked_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
||||
eventName: 'artifact.linked',
|
||||
eventVersion: 1,
|
||||
occurredAt: now,
|
||||
productId: 'notelett',
|
||||
sourceSurface: 'backend',
|
||||
userId,
|
||||
orgId: transcriptArtifact.ownership.orgId ?? null,
|
||||
sessionId: transcriptArtifact.provenance.sessionId ?? null,
|
||||
runId,
|
||||
artifactId: ecosystemNoteArtifact.id,
|
||||
actor: {
|
||||
actorType: 'agent',
|
||||
actorId: 'phase1-transcript-importer',
|
||||
},
|
||||
trace: {
|
||||
correlationId,
|
||||
causationId: createdEvent.eventId,
|
||||
parentEventId: createdEvent.eventId,
|
||||
},
|
||||
payload: {
|
||||
sourceArtifactId: ecosystemNoteArtifact.id,
|
||||
targetArtifactId: transcriptArtifact.id,
|
||||
relation: 'summarizes',
|
||||
},
|
||||
};
|
||||
|
||||
return { note, noteArtifactDoc, ecosystemNoteArtifact, createdEvent, linkedEvent };
|
||||
}
|
||||
|
||||
export async function persistPhase1NoteOutputs(params: {
|
||||
ecosystemNoteArtifact: NoteArtifactEnvelope;
|
||||
createdEvent: EcosystemEvent;
|
||||
linkedEvent: EcosystemEvent;
|
||||
root?: string;
|
||||
}): Promise<void> {
|
||||
const root = params.root ?? getPhase1Root();
|
||||
await writeJson(join(root, 'artifacts', 'note', `${params.ecosystemNoteArtifact.id}.json`), params.ecosystemNoteArtifact);
|
||||
await writeJson(join(root, 'events', 'artifact.created', `${params.createdEvent.eventId}.json`), params.createdEvent);
|
||||
await writeJson(join(root, 'events', 'artifact.linked', `${params.linkedEvent.eventId}.json`), params.linkedEvent);
|
||||
await writeJson(join(root, 'indexes', 'latest-note.json'), params.ecosystemNoteArtifact);
|
||||
}
|
||||
|
||||
async function writeJson(path: string, payload: unknown): Promise<void> {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
||||
}
|
||||
44
backend/src/modules/ecosystem-phase1/routes.ts
Normal file
44
backend/src/modules/ecosystem-phase1/routes.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { BadRequestError } from '@bytelyst/errors';
|
||||
import { requireWriter } from '../../lib/auth.js';
|
||||
import { createNote } from '../notes/repository.js';
|
||||
import { createNoteArtifact } from '../note-artifacts/repository.js';
|
||||
import {
|
||||
buildPhase1NoteImport,
|
||||
loadLatestTranscriptArtifact,
|
||||
persistPhase1NoteOutputs,
|
||||
} from '../../lib/ecosystem-phase1.js';
|
||||
|
||||
const ImportLatestTranscriptSchema = z.object({
|
||||
workspaceId: z.string().min(1).max(128),
|
||||
});
|
||||
|
||||
export async function ecosystemPhase1Routes(app: FastifyInstance) {
|
||||
app.post('/ecosystem/phase1/import-latest-transcript', async req => {
|
||||
const auth = await requireWriter(req);
|
||||
const parsed = ImportLatestTranscriptSchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
|
||||
const transcriptArtifact = await loadLatestTranscriptArtifact();
|
||||
const generated = buildPhase1NoteImport({
|
||||
transcriptArtifact,
|
||||
workspaceId: parsed.data.workspaceId,
|
||||
userId: auth.sub,
|
||||
});
|
||||
|
||||
const note = await createNote(generated.note);
|
||||
const noteArtifactDoc = await createNoteArtifact(generated.noteArtifactDoc);
|
||||
await persistPhase1NoteOutputs(generated);
|
||||
|
||||
return {
|
||||
note,
|
||||
noteArtifact: noteArtifactDoc,
|
||||
ecosystemArtifactId: generated.ecosystemNoteArtifact.id,
|
||||
linkedTranscriptArtifactId: transcriptArtifact.id,
|
||||
events: [generated.createdEvent.eventId, generated.linkedEvent.eventId],
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { createServiceApp, registerOptionalJwtContext, startService } from '@bytelyst/fastify-core';
|
||||
import { jwtVerify } from 'jose';
|
||||
import { noteAgentActionRoutes } from './modules/note-agent-actions/routes.js';
|
||||
import { ecosystemPhase1Routes } from './modules/ecosystem-phase1/routes.js';
|
||||
import { noteArtifactRoutes } from './modules/note-artifacts/routes.js';
|
||||
import { noteRoutes } from './modules/notes/routes.js';
|
||||
import { noteRelationshipRoutes } from './modules/note-relationships/routes.js';
|
||||
@ -51,6 +52,7 @@ async function registerApiPlugin(plugin: unknown) {
|
||||
}
|
||||
|
||||
await registerApiPlugin(noteAgentActionRoutes);
|
||||
await registerApiPlugin(ecosystemPhase1Routes);
|
||||
await registerApiPlugin(noteArtifactRoutes);
|
||||
await registerApiPlugin(noteRoutes);
|
||||
await registerApiPlugin(noteRelationshipRoutes);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user