import { Effect, Schema } from "effect"; const MeaningfulString = Schema.String.check( Schema.makeFilter((value) => value.trim().length > 0, { expected: "a non-empty string", }) ); export const SignalId = MeaningfulString.pipe(Schema.brand("SignalId")); export type SignalId = typeof SignalId.Type; export const OrganizationId = MeaningfulString.pipe( Schema.brand("OrganizationId") ); export type OrganizationId = typeof OrganizationId.Type; export const ProjectId = MeaningfulString.pipe(Schema.brand("ProjectId")); export type ProjectId = typeof ProjectId.Type; export const ConversationId = MeaningfulString.pipe( Schema.brand("ConversationId") ); export type ConversationId = typeof ConversationId.Type; export const ConversationMessageId = MeaningfulString.pipe( Schema.brand("ConversationMessageId") ); export type ConversationMessageId = typeof ConversationMessageId.Type; export const TimestampMs = Schema.Int.check( Schema.isGreaterThanOrEqualTo(0) ).pipe(Schema.brand("TimestampMs")); export type TimestampMs = typeof TimestampMs.Type; export const OrganizationSignalScope = Schema.Struct({ _tag: Schema.Literal("Organization"), organizationId: OrganizationId, }); export type OrganizationSignalScope = typeof OrganizationSignalScope.Type; export const ProjectSignalScope = Schema.Struct({ _tag: Schema.Literal("Project"), organizationId: OrganizationId, projectId: ProjectId, }); export type ProjectSignalScope = typeof ProjectSignalScope.Type; export const SignalScope = Schema.Union([ OrganizationSignalScope, ProjectSignalScope, ]); export type SignalScope = typeof SignalScope.Type; export const SignalSourceMessage = Schema.Struct({ conversationId: ConversationId, createdAt: TimestampMs, messageId: ConversationMessageId, rawText: Schema.String, }); export type SignalSourceMessage = typeof SignalSourceMessage.Type; export const ProblemStatement = Schema.Struct({ constraints: Schema.Array(MeaningfulString), desiredOutcome: MeaningfulString, summary: MeaningfulString, title: MeaningfulString, }); export type ProblemStatement = typeof ProblemStatement.Type; export const ProcessedByAgent = Schema.Struct({ agentInstanceId: MeaningfulString, agentName: MeaningfulString, }); export type ProcessedByAgent = typeof ProcessedByAgent.Type; const signalFields = { createdAt: TimestampMs, id: SignalId, problemStatement: ProblemStatement, processedBy: ProcessedByAgent, scope: SignalScope, sourceMessages: Schema.NonEmptyArray(SignalSourceMessage), } as const; export const SignalInput = Schema.Struct(signalFields); export type SignalInput = typeof SignalInput.Type; export const Signal = Schema.Struct(signalFields); export type Signal = typeof Signal.Type; export const SignalValidationReason = Schema.Literals([ "InvalidInput", "DuplicateSource", "MixedConversation", "OutOfOrderSource", ]); export type SignalValidationReason = typeof SignalValidationReason.Type; export class SignalValidationError extends Schema.TaggedErrorClass()( "SignalValidationError", { message: Schema.String, reason: SignalValidationReason, } ) {} export const composeSignal = Effect.fn("Signal.compose")( function* composeSignal(input: unknown) { const signal: Signal = yield* Schema.decodeUnknownEffect(SignalInput)( input ).pipe( Effect.mapError( (cause) => new SignalValidationError({ message: cause.message, reason: "InvalidInput", }) ) ); const [{ conversationId, createdAt: firstCreatedAt }] = signal.sourceMessages; let previousCreatedAt = firstCreatedAt; const messageIds = new Set(); for (const source of signal.sourceMessages) { if (source.conversationId !== conversationId) { return yield* Effect.fail( new SignalValidationError({ message: "Signal sources must belong to one conversation", reason: "MixedConversation", }) ); } if (messageIds.has(source.messageId)) { return yield* Effect.fail( new SignalValidationError({ message: "Signal sources must have unique message identities", reason: "DuplicateSource", }) ); } messageIds.add(source.messageId); if (source.createdAt < previousCreatedAt) { return yield* Effect.fail( new SignalValidationError({ message: "Signal source timestamps must be non-decreasing", reason: "OutOfOrderSource", }) ); } previousCreatedAt = source.createdAt; } return signal; } );