From 78918fbd90d10bc28874fe88ecfa79dbb3ac02c0 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Fri, 3 Apr 2026 19:30:11 -0700 Subject: [PATCH] feat(events): add phase2 plan routine habit contracts --- packages/events/src/ecosystem.test.ts | 233 ++++++++++++++++++++++++++ packages/events/src/ecosystem.ts | 77 ++++++++- 2 files changed, 308 insertions(+), 2 deletions(-) diff --git a/packages/events/src/ecosystem.test.ts b/packages/events/src/ecosystem.test.ts index 31ce87cf..c1de536e 100644 --- a/packages/events/src/ecosystem.test.ts +++ b/packages/events/src/ecosystem.test.ts @@ -7,9 +7,14 @@ import artifactCreatedEvent from '../fixtures/ecosystem/phase1/artifact-created. import artifactLinkedEvent from '../fixtures/ecosystem/phase1/artifact-linked.event.json' with { type: 'json' }; import memoryEntryCreatedEvent from '../fixtures/ecosystem/phase1/memory-entry-created.event.json' with { type: 'json' }; import { + ArtifactCreatedEventSchema, + ArtifactLinkedEventSchema, + HabitArtifactEnvelopeSchema, + PlanArtifactEnvelopeSchema, Phase1ArtifactEnvelopeSchema, Phase1EcosystemEventSchema, Phase1EcosystemEventSchemas, + RoutineArtifactEnvelopeSchema, } from './ecosystem.js'; describe('phase1 ecosystem contracts', () => { @@ -57,3 +62,231 @@ describe('phase1 ecosystem contracts', () => { expect(linked.payload.relation).toBe('summarizes'); }); }); + +describe('phase2 ecosystem contract extensions', () => { + it('validates canonical plan, routine, and habit artifacts', () => { + const plan = PlanArtifactEnvelopeSchema.parse({ + id: 'art_plan_demo', + artifactType: 'plan', + schemaVersion: 1, + productId: 'flowmonk', + sourceSurface: 'backend', + title: 'FlowMonk weekly plan', + summary: 'Three focused execution blocks', + createdAt: '2026-04-03T12:00:00.000Z', + updatedAt: '2026-04-03T12:00:00.000Z', + createdBy: { actorType: 'agent', actorId: 'phase2-plan-exporter' }, + ownership: { userId: 'saravana', orgId: null }, + visibility: { + scope: 'private', + allowedProducts: ['learning_ai_clock', 'learning_ai_efforise'], + }, + status: 'draft', + tags: ['ecosystem', 'phase2', 'plan'], + links: [], + provenance: { + originProductId: 'flowmonk', + originActionId: 'plan_export_1', + sessionId: 'sess_phase2', + runId: 'run_phase2_plan', + approvalId: null, + correlationId: 'corr_phase2', + lineage: [ + { + stepType: 'plan-exported', + productId: 'flowmonk', + actorType: 'agent', + timestamp: '2026-04-03T12:00:00.000Z', + }, + ], + }, + payload: { + weekOf: '2026-04-07', + taskCount: 3, + scheduledEntryCount: 3, + totalScheduledMinutes: 165, + entries: [ + { + taskTitle: 'Architecture review', + scheduledDate: '2026-04-07', + startTime: '08:00', + endTime: '09:00', + durationMinutes: 60, + flowName: 'Deep Work', + zoneName: 'Studio', + priority: 'high', + }, + ], + }, + }); + + const routine = RoutineArtifactEnvelopeSchema.parse({ + id: 'art_routine_demo', + artifactType: 'routine', + schemaVersion: 1, + productId: 'chronomind', + sourceSurface: 'backend', + title: 'Routine from FlowMonk weekly plan', + summary: 'Three-step execution routine', + createdAt: '2026-04-03T12:05:00.000Z', + updatedAt: '2026-04-03T12:05:00.000Z', + createdBy: { actorType: 'agent', actorId: 'phase2-routine-importer' }, + ownership: { userId: 'saravana', orgId: null }, + visibility: { scope: 'private' }, + status: 'ready', + tags: ['ecosystem', 'phase2', 'routine'], + links: [{ relation: 'generated-routine', targetArtifactId: plan.id }], + provenance: { + originProductId: 'flowmonk', + originActionId: 'plan_export_1', + sessionId: 'sess_phase2', + runId: 'run_phase2_routine', + approvalId: null, + correlationId: 'corr_phase2', + lineage: [ + { + stepType: 'plan-exported', + productId: 'flowmonk', + actorType: 'agent', + timestamp: '2026-04-03T12:00:00.000Z', + }, + { + stepType: 'routine-created', + productId: 'chronomind', + actorType: 'agent', + timestamp: '2026-04-03T12:05:00.000Z', + }, + ], + }, + payload: { + routineId: 'routine_demo', + stepCount: 3, + totalDurationMinutes: 165, + status: 'ready', + isTemplate: true, + category: 'phase2-import', + steps: [ + { + label: 'Architecture review', + durationMinutes: 60, + transition: '5m_break', + status: 'pending', + }, + ], + }, + }); + + const habit = HabitArtifactEnvelopeSchema.parse({ + id: 'art_habit_demo', + artifactType: 'habit', + schemaVersion: 1, + productId: 'efforise', + sourceSurface: 'backend', + title: 'Habit from FlowMonk weekly plan', + summary: 'Practice the imported routine daily', + createdAt: '2026-04-03T12:10:00.000Z', + updatedAt: '2026-04-03T12:10:00.000Z', + createdBy: { actorType: 'agent', actorId: 'phase2-habit-importer' }, + ownership: { userId: 'saravana', orgId: null }, + visibility: { scope: 'private' }, + status: 'active', + tags: ['ecosystem', 'phase2', 'habit'], + links: [{ relation: 'generated-habit', targetArtifactId: routine.id }], + provenance: { + originProductId: 'flowmonk', + originActionId: 'plan_export_1', + sessionId: 'sess_phase2', + runId: 'run_phase2_habit', + approvalId: null, + correlationId: 'corr_phase2', + lineage: [ + { + stepType: 'plan-exported', + productId: 'flowmonk', + actorType: 'agent', + timestamp: '2026-04-03T12:00:00.000Z', + }, + { + stepType: 'routine-created', + productId: 'chronomind', + actorType: 'agent', + timestamp: '2026-04-03T12:05:00.000Z', + }, + { + stepType: 'habit-created', + productId: 'efforise', + actorType: 'agent', + timestamp: '2026-04-03T12:10:00.000Z', + }, + ], + }, + payload: { + habitId: 'habit_demo', + identityId: 'identity_phase2', + frequency: 'daily', + targetCount: 1, + reminderTime: '08:00', + isActive: true, + sourceRoutineId: 'routine_demo', + }, + }); + + expect(routine.links[0]?.targetArtifactId).toBe(plan.id); + expect(habit.links[0]?.targetArtifactId).toBe(routine.id); + }); + + it('accepts generic artifact.created and artifact.linked events for phase2 artifact types', () => { + const created = ArtifactCreatedEventSchema.parse({ + eventId: 'evt_phase2_created', + eventName: 'artifact.created', + eventVersion: 1, + occurredAt: '2026-04-03T12:05:00.000Z', + productId: 'chronomind', + sourceSurface: 'backend', + userId: 'saravana', + orgId: null, + sessionId: 'sess_phase2', + runId: 'run_phase2_routine', + artifactId: 'art_routine_demo', + actor: { actorType: 'agent', actorId: 'phase2-routine-importer' }, + trace: { + correlationId: 'corr_phase2', + causationId: 'evt_phase2_plan', + parentEventId: 'evt_phase2_plan', + }, + payload: { + artifactType: 'routine', + title: 'Routine from FlowMonk weekly plan', + status: 'ready', + }, + }); + + const linked = ArtifactLinkedEventSchema.parse({ + eventId: 'evt_phase2_linked', + eventName: 'artifact.linked', + eventVersion: 1, + occurredAt: '2026-04-03T12:10:00.000Z', + productId: 'efforise', + sourceSurface: 'backend', + userId: 'saravana', + orgId: null, + sessionId: 'sess_phase2', + runId: 'run_phase2_habit', + artifactId: 'art_habit_demo', + actor: { actorType: 'agent', actorId: 'phase2-habit-importer' }, + trace: { + correlationId: 'corr_phase2', + causationId: 'evt_phase2_created', + parentEventId: 'evt_phase2_created', + }, + payload: { + sourceArtifactId: 'art_habit_demo', + targetArtifactId: 'art_routine_demo', + relation: 'generated-habit', + }, + }); + + expect(created.payload.artifactType).toBe('routine'); + expect(linked.payload.relation).toBe('generated-habit'); + }); +}); diff --git a/packages/events/src/ecosystem.ts b/packages/events/src/ecosystem.ts index 8ff5e44c..f72435fe 100644 --- a/packages/events/src/ecosystem.ts +++ b/packages/events/src/ecosystem.ts @@ -6,6 +6,7 @@ export const EcosystemArtifactTypeSchema = z.enum([ 'memory', 'plan', 'routine', + 'habit', 'habit-checkin', 'trail-report', 'route-session', @@ -19,6 +20,7 @@ export const ArtifactLinkRelationSchema = z.enum([ 'summarizes', 'generated-task', 'generated-routine', + 'generated-habit', 'generated-memory', 'evidence-for', 'review-of', @@ -113,6 +115,53 @@ export const MemoryPayloadSchema = z.object({ reviewState: z.enum(['proposed', 'accepted', 'rejected']), }); +export const PlanPayloadSchema = z.object({ + weekOf: z.string().min(1), + taskCount: z.number().int().nonnegative(), + scheduledEntryCount: z.number().int().nonnegative(), + totalScheduledMinutes: z.number().int().nonnegative(), + entries: z.array( + z.object({ + taskTitle: z.string().min(1), + scheduledDate: z.string().min(1), + startTime: z.string().min(1), + endTime: z.string().min(1), + durationMinutes: z.number().int().nonnegative(), + flowName: z.string().min(1).nullable(), + zoneName: z.string().min(1).nullable(), + priority: z.string().min(1), + }) + ), +}); + +export const RoutinePayloadSchema = z.object({ + routineId: z.string().min(1), + stepCount: z.number().int().nonnegative(), + totalDurationMinutes: z.number().nonnegative(), + status: z.string().min(1), + isTemplate: z.boolean(), + category: z.string().min(1).nullable().optional(), + steps: z.array( + z.object({ + label: z.string().min(1), + durationMinutes: z.number().nonnegative(), + transition: z.string().min(1), + status: z.string().min(1), + }) + ), +}); + +export const HabitPayloadSchema = z.object({ + habitId: z.string().min(1), + identityId: z.string().min(1), + frequency: z.enum(['daily', 'weekly', 'custom']), + customDays: z.array(z.number().int().min(0).max(6)).optional(), + targetCount: z.number().int().positive(), + reminderTime: z.string().min(1).nullable().optional(), + isActive: z.boolean(), + sourceRoutineId: z.string().min(1), +}); + export const TranscriptArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({ artifactType: z.literal('transcript'), payload: TranscriptPayloadSchema, @@ -128,6 +177,21 @@ export const MemoryArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({ payload: MemoryPayloadSchema, }); +export const PlanArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({ + artifactType: z.literal('plan'), + payload: PlanPayloadSchema, +}); + +export const RoutineArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({ + artifactType: z.literal('routine'), + payload: RoutinePayloadSchema, +}); + +export const HabitArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({ + artifactType: z.literal('habit'), + payload: HabitPayloadSchema, +}); + export const Phase1ArtifactEnvelopeSchema = z.discriminatedUnion('artifactType', [ TranscriptArtifactEnvelopeSchema, NoteArtifactEnvelopeSchema, @@ -170,7 +234,7 @@ export const CaptureTranscriptCreatedPayloadSchema = z.object({ }); export const ArtifactCreatedPayloadSchema = z.object({ - artifactType: z.enum(['transcript', 'note', 'memory']), + artifactType: z.enum(['transcript', 'note', 'memory', 'plan', 'routine', 'habit']), title: z.string().min(1).nullable(), status: z.string().min(1), }); @@ -178,7 +242,13 @@ export const ArtifactCreatedPayloadSchema = z.object({ export const ArtifactLinkedPayloadSchema = z.object({ sourceArtifactId: z.string().min(1), targetArtifactId: z.string().min(1), - relation: z.enum(['summarizes', 'generated-memory']), + relation: z.enum([ + 'summarizes', + 'generated-memory', + 'generated-routine', + 'generated-habit', + 'derived-from', + ]), }); export const MemoryEntryCreatedPayloadSchema = z.object({ @@ -227,6 +297,9 @@ export type ArtifactEnvelope = z.infer; export type TranscriptArtifactEnvelope = z.infer; export type NoteArtifactEnvelope = z.infer; export type MemoryArtifactEnvelope = z.infer; +export type PlanArtifactEnvelope = z.infer; +export type RoutineArtifactEnvelope = z.infer; +export type HabitArtifactEnvelope = z.infer; export type Phase1ArtifactEnvelope = z.infer; export type EcosystemEvent = z.infer; export type Phase1EcosystemEvent = z.infer;