// PhaseRelease — one authored phase expands to ordinary per-task Release // constraints. The engine wire stays the existing Release ADT; the block // compiler owns the one-to-many expansion and gives each emitted constraint // an attributed sub-instance name. import { z } from "zod"; import type { ReleaseConstraint } from "../level-core/types.ts"; import type { MiniZincFragment, MultiConstraintBlock } from "./types.ts"; export const PhaseReleaseInputSchema = z .object({ taskUniqueIds: z.array(z.number().int()).min(1), earliestStart: z.number().int().nonnegative(), }) .refine( (input) => new Set(input.taskUniqueIds).size === input.taskUniqueIds.length, { message: "taskUniqueIds must be unique within a phase", path: ["taskUniqueIds"], }, ); export type PhaseReleaseInput = z.infer; const ReleaseOutputSchema: z.ZodType = z.object({ kind: z.literal("Release"), taskUniqueId: z.number().int(), earliestStart: z.number().int().nonnegative(), }); export const PhaseReleaseBlock: MultiConstraintBlock = { id: "PhaseRelease", schema: { input: PhaseReleaseInputSchema, output: z.array(ReleaseOutputSchema), }, apply: ({ taskUniqueIds, earliestStart }): ReleaseConstraint[] => taskUniqueIds.map((taskUniqueId) => ({ kind: "Release", taskUniqueId, earliestStart, })), toMiniZinc: ({ taskUniqueIds, earliestStart }): MiniZincFragment => ({ text: taskUniqueIds .map( (taskUniqueId) => `constraint start_day[${String(taskUniqueId)}] >= ${String(earliestStart)};`, ) .join("\n"), }), doc: { nl: "Every task in the phase may start no earlier than the shared day index.", pseudocode: "for task in phase: start(task) >= earliestStart", }, };