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 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 memoryEntryCreatedEvent from '../fixtures/ecosystem/phase1/memory-entry-created.event.json' with { type: 'json' };
|
||||||
import {
|
import {
|
||||||
|
ArtifactCreatedEventSchema,
|
||||||
|
ArtifactLinkedEventSchema,
|
||||||
|
HabitArtifactEnvelopeSchema,
|
||||||
|
PlanArtifactEnvelopeSchema,
|
||||||
Phase1ArtifactEnvelopeSchema,
|
Phase1ArtifactEnvelopeSchema,
|
||||||
Phase1EcosystemEventSchema,
|
Phase1EcosystemEventSchema,
|
||||||
Phase1EcosystemEventSchemas,
|
Phase1EcosystemEventSchemas,
|
||||||
|
RoutineArtifactEnvelopeSchema,
|
||||||
} from './ecosystem.js';
|
} from './ecosystem.js';
|
||||||
|
|
||||||
describe('phase1 ecosystem contracts', () => {
|
describe('phase1 ecosystem contracts', () => {
|
||||||
@ -57,3 +62,231 @@ describe('phase1 ecosystem contracts', () => {
|
|||||||
expect(linked.payload.relation).toBe('summarizes');
|
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',
|
'memory',
|
||||||
'plan',
|
'plan',
|
||||||
'routine',
|
'routine',
|
||||||
|
'habit',
|
||||||
'habit-checkin',
|
'habit-checkin',
|
||||||
'trail-report',
|
'trail-report',
|
||||||
'route-session',
|
'route-session',
|
||||||
@ -19,6 +20,7 @@ export const ArtifactLinkRelationSchema = z.enum([
|
|||||||
'summarizes',
|
'summarizes',
|
||||||
'generated-task',
|
'generated-task',
|
||||||
'generated-routine',
|
'generated-routine',
|
||||||
|
'generated-habit',
|
||||||
'generated-memory',
|
'generated-memory',
|
||||||
'evidence-for',
|
'evidence-for',
|
||||||
'review-of',
|
'review-of',
|
||||||
@ -113,6 +115,53 @@ export const MemoryPayloadSchema = z.object({
|
|||||||
reviewState: z.enum(['proposed', 'accepted', 'rejected']),
|
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({
|
export const TranscriptArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({
|
||||||
artifactType: z.literal('transcript'),
|
artifactType: z.literal('transcript'),
|
||||||
payload: TranscriptPayloadSchema,
|
payload: TranscriptPayloadSchema,
|
||||||
@ -128,6 +177,21 @@ export const MemoryArtifactEnvelopeSchema = BaseArtifactEnvelopeSchema.extend({
|
|||||||
payload: MemoryPayloadSchema,
|
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', [
|
export const Phase1ArtifactEnvelopeSchema = z.discriminatedUnion('artifactType', [
|
||||||
TranscriptArtifactEnvelopeSchema,
|
TranscriptArtifactEnvelopeSchema,
|
||||||
NoteArtifactEnvelopeSchema,
|
NoteArtifactEnvelopeSchema,
|
||||||
@ -170,7 +234,7 @@ export const CaptureTranscriptCreatedPayloadSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ArtifactCreatedPayloadSchema = 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(),
|
title: z.string().min(1).nullable(),
|
||||||
status: z.string().min(1),
|
status: z.string().min(1),
|
||||||
});
|
});
|
||||||
@ -178,7 +242,13 @@ export const ArtifactCreatedPayloadSchema = z.object({
|
|||||||
export const ArtifactLinkedPayloadSchema = z.object({
|
export const ArtifactLinkedPayloadSchema = z.object({
|
||||||
sourceArtifactId: z.string().min(1),
|
sourceArtifactId: z.string().min(1),
|
||||||
targetArtifactId: 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({
|
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 TranscriptArtifactEnvelope = z.infer<typeof TranscriptArtifactEnvelopeSchema>;
|
||||||
export type NoteArtifactEnvelope = z.infer<typeof NoteArtifactEnvelopeSchema>;
|
export type NoteArtifactEnvelope = z.infer<typeof NoteArtifactEnvelopeSchema>;
|
||||||
export type MemoryArtifactEnvelope = z.infer<typeof MemoryArtifactEnvelopeSchema>;
|
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 Phase1ArtifactEnvelope = z.infer<typeof Phase1ArtifactEnvelopeSchema>;
|
||||||
export type EcosystemEvent = z.infer<typeof BaseEcosystemEventSchema>;
|
export type EcosystemEvent = z.infer<typeof BaseEcosystemEventSchema>;
|
||||||
export type Phase1EcosystemEvent = z.infer<typeof Phase1EcosystemEventSchema>;
|
export type Phase1EcosystemEvent = z.infer<typeof Phase1EcosystemEventSchema>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user