Files
zopu-code/packages/primitives/src/signal.ts
2026-07-23 15:15:00 +05:30

160 lines
4.6 KiB
TypeScript

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>()(
"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<ConversationMessageId>();
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;
}
);