diff --git a/backend/src/lib/ecosystem-phase3.test.ts b/backend/src/lib/ecosystem-phase3.test.ts new file mode 100644 index 0000000..09e4a0f --- /dev/null +++ b/backend/src/lib/ecosystem-phase3.test.ts @@ -0,0 +1,90 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + buildPhase3TrailNoteImport, + loadLatestTrailReportArtifact, + loadLatestTrailReportCreatedEvent, + persistPhase3NoteOutputs, +} from './ecosystem-phase3.ts'; + +test('builds and persists a NoteLett audit note from the latest trail report artifact', async () => { + const root = await mkdtemp(join(tmpdir(), 'notelett-phase3-')); + await mkdir(join(root, 'indexes'), { recursive: true }); + await writeFile( + join(root, 'indexes', 'latest-trail-report.json'), + `${JSON.stringify({ + id: 'art_trail_demo', + title: 'Cowork audit report for task task_phase3_demo', + summary: '3 audited actions, 1 safety signal', + ownership: { userId: 'saravana', orgId: null }, + provenance: { + sessionId: 'sess_phase3', + correlationId: 'corr_phase3', + lineage: [ + { + stepType: 'trail-report-created', + productId: 'actiontrail', + actorType: 'agent', + timestamp: '2026-04-03T14:05:00.000Z', + }, + ], + }, + payload: { + sourceProduct: 'claw-cowork', + sourceTaskId: 'task_phase3_demo', + reportGeneratedAt: '2026-04-03T14:05:00.000Z', + actionCount: 3, + toolCallCount: 1, + approvalCount: 1, + failureCount: 0, + safetySignalCount: 1, + actionBreakdown: [ + { action: 'ToolCall', count: 1 }, + { action: 'InjectionDetected', count: 1 }, + ], + entries: [ + { + timestamp: '2026-04-03T14:01:00.000Z', + taskId: 'task_phase3_demo', + action: 'ToolCall', + tool: 'search_query', + inputSummary: 'Find roadmap delta', + result: 'Success', + }, + ], + }, + }, null, 2)}\n`, + 'utf-8' + ); + await writeFile( + join(root, 'indexes', 'latest-trail-report-created-event.json'), + `${JSON.stringify({ eventId: 'evt_trail_created_demo', eventName: 'artifact.created', trace: { correlationId: 'corr_phase3', causationId: 'task_phase3_demo', parentEventId: 'task_phase3_demo' } }, null, 2)}\n`, + 'utf-8' + ); + + const trailReportArtifact = await loadLatestTrailReportArtifact(root); + const trailReportCreatedEvent = await loadLatestTrailReportCreatedEvent(root); + const generated = buildPhase3TrailNoteImport({ + trailReportArtifact, + trailReportCreatedEvent, + workspaceId: 'ws_phase3', + userId: 'saravana', + root, + now: '2026-04-03T14:10:00.000Z', + }); + + assert.equal(generated.note.sourceType, 'ecosystem-trail-report'); + assert.equal(generated.createdEvent.trace.causationId, 'evt_trail_created_demo'); + assert.equal(generated.linkedEvent.payload.relation, 'summarizes'); + + await persistPhase3NoteOutputs({ ...generated, root }); + + const linkedEvent = JSON.parse( + await readFile(join(root, 'indexes', 'latest-note-linked-event.json'), 'utf-8') + ) as { payload: { targetArtifactId: string } }; + + assert.equal(linkedEvent.payload.targetArtifactId, 'art_trail_demo'); +}); diff --git a/backend/src/lib/ecosystem-phase3.ts b/backend/src/lib/ecosystem-phase3.ts new file mode 100644 index 0000000..8b0a774 --- /dev/null +++ b/backend/src/lib/ecosystem-phase3.ts @@ -0,0 +1,353 @@ +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'; + +const DEFAULT_PHASE3_ROOT = join(homedir(), '.bytelyst', 'ecosystem', 'phase3'); + +type TrailReportArtifact = { + id: string; + title: string; + summary: string; + ownership: { + userId: string; + orgId?: string | null; + }; + provenance: { + sessionId?: string | null; + correlationId?: string | null; + lineage: Array<{ + stepType: string; + productId: string; + actorType: 'user' | 'agent' | 'system'; + timestamp: string; + }>; + }; + payload: { + sourceProduct: 'claw-cowork'; + sourceTaskId?: string | null; + reportGeneratedAt: string; + actionCount: number; + toolCallCount: number; + approvalCount: number; + failureCount: number; + safetySignalCount: number; + actionBreakdown: Array<{ + action: string; + count: number; + }>; + entries: Array<{ + timestamp: string; + taskId: string | null; + action: string; + tool?: string | null; + result?: string | null; + approval?: string | null; + inputSummary?: string | null; + }>; + }; +}; + +type TrailReportCreatedEvent = { + eventId: string; + eventName: 'artifact.created'; + trace: { + correlationId: string | null; + causationId: string | null; + parentEventId: string | null; + }; +}; + +type NoteArtifactEnvelope = { + id: string; + artifactType: 'note'; + schemaVersion: 1; + productId: 'notelett'; + sourceSurface: 'backend'; + title: string; + summary: string; + createdAt: string; + updatedAt: string; + createdBy: { + actorType: 'agent'; + actorId: string; + }; + ownership: { + userId: string; + orgId?: string | null; + }; + visibility: { + scope: 'private'; + allowedProducts: ['learning_multimodal_memory_agents']; + }; + status: 'draft'; + tags: string[]; + links: Array<{ + relation: 'summarizes'; + targetArtifactId: string; + }>; + provenance: TrailReportArtifact['provenance'] & { + runId: string; + }; + payload: { + noteFormat: 'markdown'; + body: string; + excerpt: string; + }; +}; + +type EcosystemEvent = { + eventId: string; + eventName: 'artifact.created' | 'artifact.linked'; + eventVersion: 1; + occurredAt: string; + productId: 'notelett'; + sourceSurface: 'backend'; + userId: string; + orgId?: string | null; + sessionId?: string | null; + runId: string; + artifactId: string; + actor: { + actorType: 'agent'; + actorId: string; + }; + trace: { + correlationId: string | null; + causationId: string | null; + parentEventId: string | null; + }; + payload: Record; +}; + +export function getPhase3Root(): string { + return process.env.BYTELYST_ECOSYSTEM_DIR ?? DEFAULT_PHASE3_ROOT; +} + +export async function loadLatestTrailReportArtifact(root = getPhase3Root()): Promise { + const raw = await readFile(join(root, 'indexes', 'latest-trail-report.json'), 'utf-8'); + return JSON.parse(raw) as TrailReportArtifact; +} + +export async function loadLatestTrailReportCreatedEvent( + root = getPhase3Root() +): Promise { + try { + const raw = await readFile(join(root, 'indexes', 'latest-trail-report-created-event.json'), 'utf-8'); + return JSON.parse(raw) as TrailReportCreatedEvent; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } + throw error; + } +} + +export function buildPhase3TrailNoteImport(params: { + trailReportArtifact: TrailReportArtifact; + trailReportCreatedEvent?: TrailReportCreatedEvent | null; + workspaceId: string; + userId: string; + root?: string; + now?: string; +}) { + const now = params.now ?? new Date().toISOString(); + const noteId = `note_phase3_${randomUUID().replace(/-/g, '').slice(0, 12)}`; + const noteArtifactId = `art_note_${randomUUID().replace(/-/g, '').slice(0, 12)}`; + const runId = `run_note_${randomUUID().replace(/-/g, '').slice(0, 10)}`; + const title = params.trailReportArtifact.title || 'Imported audit note'; + const breakdown = params.trailReportArtifact.payload.actionBreakdown + .slice(0, 5) + .map(item => `- ${item.action}: ${item.count}`) + .join('\n'); + const evidence = params.trailReportArtifact.payload.entries + .slice(0, 3) + .map( + entry => + `- ${entry.timestamp} | ${entry.action}${entry.tool ? ` (${entry.tool})` : ''}${entry.inputSummary ? ` — ${entry.inputSummary}` : ''}` + ) + .join('\n'); + const body = [ + `# ${title}`, + '', + params.trailReportArtifact.summary, + '', + `Source product: ${params.trailReportArtifact.payload.sourceProduct}`, + `Source task: ${params.trailReportArtifact.payload.sourceTaskId ?? 'n/a'}`, + `Action count: ${params.trailReportArtifact.payload.actionCount}`, + `Safety signals: ${params.trailReportArtifact.payload.safetySignalCount}`, + '', + '## Action Breakdown', + breakdown || '- none', + '', + '## Evidence', + evidence || '- none', + ].join('\n'); + const excerpt = `${params.trailReportArtifact.payload.actionCount} audited actions imported from Cowork`; + + const note: NoteDoc = { + id: noteId, + productId: 'notelett', + workspaceId: params.workspaceId, + userId: params.userId, + title, + body, + status: 'draft', + tags: ['ecosystem', 'phase3', 'audit'], + links: [params.trailReportArtifact.id], + sourceType: 'ecosystem-trail-report', + sourceUri: params.trailReportArtifact.id, + createdAt: now, + updatedAt: now, + createdBy: params.userId, + updatedBy: params.userId, + agentId: 'phase3-trail-importer', + }; + + const noteArtifactDoc: NoteArtifactDoc = { + id: `na_${randomUUID().replace(/-/g, '').slice(0, 12)}`, + productId: 'notelett', + workspaceId: params.workspaceId, + userId: params.userId, + noteId, + artifactType: 'summary', + title: 'Imported trail report artifact', + description: `Audit trail source ${params.trailReportArtifact.id}`, + blobPath: join(params.root ?? getPhase3Root(), 'artifacts', 'note', `${noteArtifactId}.json`), + contentType: 'application/json', + createdAt: now, + createdBy: params.userId, + updatedAt: now, + updatedBy: params.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: 'phase3-trail-importer', + }, + ownership: { + userId: params.userId, + orgId: params.trailReportArtifact.ownership.orgId ?? null, + }, + visibility: { + scope: 'private', + allowedProducts: ['learning_multimodal_memory_agents'], + }, + status: 'draft', + tags: ['ecosystem', 'phase3', 'audit'], + links: [ + { + relation: 'summarizes', + targetArtifactId: params.trailReportArtifact.id, + }, + ], + provenance: { + ...params.trailReportArtifact.provenance, + runId, + lineage: [ + ...params.trailReportArtifact.provenance.lineage, + { + stepType: 'audit-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: params.userId, + orgId: params.trailReportArtifact.ownership.orgId ?? null, + sessionId: params.trailReportArtifact.provenance.sessionId ?? null, + runId, + artifactId: ecosystemNoteArtifact.id, + actor: { + actorType: 'agent', + actorId: 'phase3-trail-importer', + }, + trace: { + correlationId: params.trailReportArtifact.provenance.correlationId ?? null, + causationId: params.trailReportCreatedEvent?.eventId ?? null, + parentEventId: params.trailReportCreatedEvent?.eventId ?? null, + }, + payload: { + artifactType: 'note', + title, + status: 'draft', + }, + }; + + 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: params.userId, + orgId: params.trailReportArtifact.ownership.orgId ?? null, + sessionId: params.trailReportArtifact.provenance.sessionId ?? null, + runId, + artifactId: ecosystemNoteArtifact.id, + actor: { + actorType: 'agent', + actorId: 'phase3-trail-importer', + }, + trace: { + correlationId: params.trailReportArtifact.provenance.correlationId ?? null, + causationId: createdEvent.eventId, + parentEventId: createdEvent.eventId, + }, + payload: { + sourceArtifactId: ecosystemNoteArtifact.id, + targetArtifactId: params.trailReportArtifact.id, + relation: 'summarizes', + }, + }; + + return { note, noteArtifactDoc, ecosystemNoteArtifact, createdEvent, linkedEvent }; +} + +export async function persistPhase3NoteOutputs(params: { + ecosystemNoteArtifact: NoteArtifactEnvelope; + createdEvent: EcosystemEvent; + linkedEvent: EcosystemEvent; + root?: string; +}) { + const root = params.root ?? getPhase3Root(); + 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); + await writeJson(join(root, 'indexes', 'latest-note-created-event.json'), params.createdEvent); + await writeJson(join(root, 'indexes', 'latest-note-linked-event.json'), params.linkedEvent); +} + +async function writeJson(path: string, payload: unknown) { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8'); +}