import { Effect, Schema } from "effect"; const Text = Schema.String.check( Schema.makeFilter((value) => value.trim().length > 0, { expected: "a non-empty string", }) ); export const DefinitionRisk = Schema.Literals(["low", "medium", "high"]); export type DefinitionRisk = typeof DefinitionRisk.Type; export const QuestionImpact = Schema.Literals(["low", "medium", "high"]); export type QuestionImpact = typeof QuestionImpact.Type; export const WorkQuestion = Schema.Struct({ alternatives: Schema.Array(Text), answer: Schema.optional(Text), id: Text, impact: QuestionImpact, prompt: Text, recommendation: Schema.optional(Text), status: Schema.Literals(["open", "answered", "withdrawn"]), }); export type WorkQuestion = typeof WorkQuestion.Type; export const WorkDefinition = Schema.Struct({ acceptanceCriteria: Schema.NonEmptyArray(Text), affectedUsers: Schema.Array(Text), assumptions: Schema.Array(Text), constraints: Schema.Array(Text), desiredOutcome: Text, inScope: Schema.Array(Text), outOfScope: Schema.Array(Text), problem: Text, questions: Schema.Array(WorkQuestion), requiredArtifacts: Schema.Array(Text), risk: DefinitionRisk, rollback: Schema.optional(Text), rollout: Schema.optional(Text), version: Schema.Int, }); export type WorkDefinition = typeof WorkDefinition.Type; export const DefinitionApproval = Schema.Struct({ approvedAt: Schema.Number, approvedBy: Text, definitionVersion: Schema.Int, }); export type DefinitionApproval = typeof DefinitionApproval.Type; export const DefinitionProposal = WorkDefinition; export type DefinitionProposal = typeof DefinitionProposal.Type; export class DefinitionError extends Schema.TaggedErrorClass()( "DefinitionError", { message: Schema.String, reason: Schema.Literals(["Invalid", "OpenHighImpactQuestion"]), } ) {} export const decodeDefinition = Effect.fn("Work.decodeDefinition")( function* decodeDefinition(input: unknown) { const definition = yield* Schema.decodeUnknownEffect(WorkDefinition)( input ).pipe( Effect.mapError( (cause) => new DefinitionError({ message: cause.message, reason: "Invalid" }) ) ); const questionIds = new Set(); for (const question of definition.questions) { if (questionIds.has(question.id)) { return yield* Effect.fail( new DefinitionError({ message: `Question ID must be unique: ${question.id}`, reason: "Invalid", }) ); } questionIds.add(question.id); } return definition; } ); export const validateDefinition = Effect.fn("Work.validateDefinition")( function* validateDefinition(input: unknown) { const definition = yield* decodeDefinition(input); const blocked = definition.questions.some( (question) => question.status === "open" && question.impact === "high" ); if (blocked) { return yield* Effect.fail( new DefinitionError({ message: "High-impact open questions must be resolved before approval", reason: "OpenHighImpactQuestion", }) ); } return definition; } ); export const canApproveDefinition = (definition: WorkDefinition): boolean => definition.questions.every( (question) => question.status !== "open" || question.impact !== "high" );