feat(events): add phase2 plan routine habit contracts

This commit is contained in:
Saravana Achu Mac 2026-04-03 19:30:11 -07:00
parent dd3636d867
commit 78918fbd90
2 changed files with 308 additions and 2 deletions

View File

@ -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');
});
});

View File

@ -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<typeof BaseArtifactEnvelopeSchema>;
export type TranscriptArtifactEnvelope = z.infer<typeof TranscriptArtifactEnvelopeSchema>;
export type NoteArtifactEnvelope = z.infer<typeof NoteArtifactEnvelopeSchema>;
export type MemoryArtifactEnvelope = z.infer<typeof MemoryArtifactEnvelopeSchema>;
export type PlanArtifactEnvelope = z.infer<typeof PlanArtifactEnvelopeSchema>;
export type RoutineArtifactEnvelope = z.infer<typeof RoutineArtifactEnvelopeSchema>;
export type HabitArtifactEnvelope = z.infer<typeof HabitArtifactEnvelopeSchema>;
export type Phase1ArtifactEnvelope = z.infer<typeof Phase1ArtifactEnvelopeSchema>;
export type EcosystemEvent = z.infer<typeof BaseEcosystemEventSchema>;
export type Phase1EcosystemEvent = z.infer<typeof Phase1EcosystemEventSchema>;