import { Array as EffectArray, Effect, Order, Schema } from "effect"; const MeaningfulString = Schema.String.check( Schema.makeFilter((value) => value.trim().length > 0, { expected: "a non-empty string", }) ); export const WorkStatus = Schema.Literal("proposed"); export type WorkStatus = typeof WorkStatus.Type; export const WorkDraft = Schema.Struct({ objective: MeaningfulString, title: MeaningfulString, }); export type WorkDraft = typeof WorkDraft.Type; export const SignalProblem = Schema.Struct({ desiredOutcome: MeaningfulString, summary: MeaningfulString, title: MeaningfulString, }); export const WorkCommandReason = Schema.Literals([ "InvalidInput", "ProjectMismatch", "TitleTooLong", "ObjectiveTooLong", ]); export type WorkCommandReason = typeof WorkCommandReason.Type; export class WorkCommandError extends Schema.TaggedErrorClass()( "WorkCommandError", { message: Schema.String, reason: WorkCommandReason, } ) {} export const workDraftFromSignal = Effect.fn("Work.draftFromSignal")( function* workDraftFromSignal(input: unknown) { const problem = yield* Schema.decodeUnknownEffect(SignalProblem)( input ).pipe( Effect.mapError( () => new WorkCommandError({ message: "Signal is missing a valid problem statement", reason: "InvalidInput", }) ) ); const title = problem.title.trim(); const objective = problem.desiredOutcome.trim(); if (title.length > 160) { return yield* new WorkCommandError({ message: "Work title must be 160 characters or fewer", reason: "TitleTooLong", }); } if (objective.length > 2000) { return yield* new WorkCommandError({ message: "Work objective must be 2000 characters or fewer", reason: "ObjectiveTooLong", }); } return { objective, title } satisfies WorkDraft; } ); const WorkIdentity = Schema.Struct({ organizationId: MeaningfulString, projectId: MeaningfulString, workId: MeaningfulString, }); const SignalIdentity = Schema.Struct({ organizationId: MeaningfulString, projectId: MeaningfulString, signalId: MeaningfulString, title: MeaningfulString, }); const WorkAttachmentInput = Schema.Struct({ signal: SignalIdentity, work: WorkIdentity, }); export const WorkEventKind = Schema.Literals([ "work.proposed", "signal.attached", ]); export type WorkEventKind = typeof WorkEventKind.Type; export const WorkEventDraft = Schema.Struct({ data: Schema.Struct({ signalId: MeaningfulString, title: MeaningfulString, }), idempotencyKey: MeaningfulString, kind: WorkEventKind, }); export type WorkEventDraft = typeof WorkEventDraft.Type; export const workProposedEventFromSignal = Effect.fn( "Work.proposedEventFromSignal" )(function* workProposedEventFromSignal(input: unknown) { const signal = yield* Schema.decodeUnknownEffect(SignalIdentity)(input).pipe( Effect.mapError( () => new WorkCommandError({ message: "Signal identity is invalid", reason: "InvalidInput", }) ) ); return { data: { signalId: signal.signalId, title: signal.title.trim() }, idempotencyKey: `proposed:${signal.signalId}`, kind: "work.proposed", } satisfies WorkEventDraft; }); export const signalAttachedEvent = Effect.fn("Work.signalAttachedEvent")( function* signalAttachedEvent(input: unknown) { const attachment = yield* Schema.decodeUnknownEffect(WorkAttachmentInput)( input ).pipe( Effect.mapError( () => new WorkCommandError({ message: "Work attachment input is invalid", reason: "InvalidInput", }) ) ); if ( attachment.signal.organizationId !== attachment.work.organizationId || attachment.signal.projectId !== attachment.work.projectId ) { return yield* new WorkCommandError({ message: "Signal and Work must belong to the same project", reason: "ProjectMismatch", }); } return { data: { signalId: attachment.signal.signalId, title: attachment.signal.title.trim(), }, idempotencyKey: `signal:${attachment.signal.signalId}`, kind: "signal.attached", } satisfies WorkEventDraft; } ); const WorkNoticeInput = Schema.Array( Schema.Struct({ _id: MeaningfulString, events: Schema.Array( Schema.Struct({ _id: MeaningfulString, createdAt: Schema.Number, kind: WorkEventKind, }) ), signals: Schema.Array( Schema.Struct({ sources: Schema.Array( Schema.Struct({ messageId: MeaningfulString, rawText: Schema.String }) ), }) ), }) ); export interface WorkNotice { readonly createdAt: number; readonly eventId: string; readonly sourceMessageIds: readonly string[]; readonly sourceTexts: readonly string[]; readonly workId: string; } const projectWorkNoticesEffect = Effect.fn("Work.projectNotices")( function* projectWorkNoticesEffect(input: unknown) { const works = yield* Schema.decodeUnknownEffect(WorkNoticeInput)( input ).pipe( Effect.mapError( () => new WorkCommandError({ message: "Work notice data is invalid", reason: "InvalidInput", }) ) ); const notices = works.flatMap((work) => { const sourceMessageIds = work.signals.flatMap((signal) => signal.sources.map((source) => source.messageId) ); const sourceTexts = work.signals.flatMap((signal) => signal.sources.map((source) => source.rawText) ); return work.events .filter((event) => event.kind === "work.proposed") .map( (event): WorkNotice => ({ createdAt: event.createdAt, eventId: event._id, sourceMessageIds, sourceTexts, workId: work._id, }) ); }); return EffectArray.sortWith( notices, (notice) => notice.createdAt, Order.Number ); } ); export const projectWorkNotices = (input: unknown): readonly WorkNotice[] => Effect.runSync(projectWorkNoticesEffect(input));