feat(events): add phase2 plan routine habit contracts
This commit is contained in:
parent
dd3636d867
commit
78918fbd90
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user