/// import { Cell, Default, handler, lift, recipe, str } from "commontools"; type ReleaseTaskStatus = "pending" | "in_progress" | "blocked" | "done"; interface ReleaseTask { id: string; label: string; required: boolean; status: ReleaseTaskStatus; owner: string | null; note: string | null; } interface ReleaseChecklistArgs { tasks: Default; } interface TaskProgressEvent { id?: string; status?: string; owner?: string | null; note?: string | null; } interface ReleaseChecklistStats { total: number; requiredTotal: number; completedRequired: number; completedTotal: number; blocked: string[]; pendingRequired: string[]; ready: boolean; } const statusValues: ReleaseTaskStatus[] = [ "pending", "in_progress", "blocked", "done", ]; const statusSet = new Set(statusValues); const defaultTasks: ReleaseTask[] = [ { id: "qa-signoff", label: "QA Sign-off", required: true, status: "pending", owner: "Jordan Patel", note: null, }, { id: "documentation", label: "Documentation Updated", required: true, status: "pending", owner: "Avery Fox", note: null, }, { id: "ops-runbook", label: "Operations Runbook", required: true, status: "pending", owner: "Taylor Young", note: null, }, { id: "marketing-review", label: "Marketing Review", required: false, status: "pending", owner: null, note: null, }, ]; const cloneTask = (task: ReleaseTask): ReleaseTask => ({ id: task.id, label: task.label, required: task.required, status: task.status, owner: task.owner ?? null, note: task.note ?? null, }); const cloneTasks = (entries: readonly ReleaseTask[]): ReleaseTask[] => entries.map((entry) => cloneTask(entry)); const taskEquals = (left: ReleaseTask, right: ReleaseTask): boolean => left.id === right.id && left.label === right.label && left.required === right.required && left.status === right.status && (left.owner ?? null) === right.owner && (left.note ?? null) === right.note; const listsEqual = ( current: unknown, sanitized: ReleaseTask[], ): current is ReleaseTask[] => { if (!Array.isArray(current) || current.length !== sanitized.length) { return false; } for (let index = 0; index < sanitized.length; index++) { if (!taskEquals(current[index] as ReleaseTask, sanitized[index])) { return false; } } return true; }; const toOptionalString = (value: unknown): string | null => { if (value === null) return null; if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }; const toStatus = (value: unknown): ReleaseTaskStatus | null => { if (typeof value !== "string") return null; const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_"); return statusSet.has(normalized as ReleaseTaskStatus) ? normalized as ReleaseTaskStatus : null; }; const toLabel = (id: string, label: unknown): string => { if (typeof label === "string") { const trimmed = label.trim(); if (trimmed.length > 0) return trimmed; } return id .split(/[-_]/g) .filter((part) => part.length > 0) .map((part) => part[0].toUpperCase() + part.slice(1)) .join(" "); }; const sanitizeTask = (value: unknown): ReleaseTask | null => { if (typeof value !== "object" || value === null) return null; const input = value as { id?: unknown; label?: unknown; required?: unknown; status?: unknown; owner?: unknown; note?: unknown; }; const rawId = typeof input.id === "string" ? input.id.trim() : ""; if (rawId.length === 0) return null; const id = rawId; const label = toLabel(id, input.label); const required = typeof input.required === "boolean" ? input.required : true; const status = toStatus(input.status) ?? "pending"; const owner = toOptionalString(input.owner); const note = toOptionalString(input.note); return { id, label, required, status, owner, note }; }; const compareTasks = (left: ReleaseTask, right: ReleaseTask): number => { if (left.required !== right.required) { return left.required ? -1 : 1; } const byLabel = left.label.localeCompare(right.label); if (byLabel !== 0) return byLabel; return left.id.localeCompare(right.id); }; const sanitizeTaskList = (value: unknown): ReleaseTask[] => { if (!Array.isArray(value)) { return cloneTasks(defaultTasks); } const seen = new Set(); const sanitized: ReleaseTask[] = []; for (const entry of value) { const task = sanitizeTask(entry); if (!task) continue; if (seen.has(task.id)) continue; seen.add(task.id); sanitized.push(task); } if (sanitized.length === 0) { return cloneTasks(defaultTasks); } sanitized.sort(compareTasks); return sanitized; }; const ensureTaskList = (source: Cell): ReleaseTask[] => { const current = source.get(); const sanitized = sanitizeTaskList(current); if (!listsEqual(current, sanitized)) { source.set(sanitized); return sanitized; } return Array.isArray(current) ? current : sanitized; }; const analyzeTasks = (tasks: ReleaseTask[]): ReleaseChecklistStats => { const blocked: string[] = []; const pendingRequired: string[] = []; let requiredTotal = 0; let completedRequired = 0; let completedTotal = 0; for (const task of tasks) { if (task.status === "blocked") { blocked.push(task.label); } if (task.status === "done") { completedTotal++; } if (task.required) { requiredTotal++; if (task.status === "done") { completedRequired++; } else { pendingRequired.push(task.label); } } } const ready = blocked.length === 0 && pendingRequired.length === 0; return { total: tasks.length, requiredTotal, completedRequired, completedTotal, blocked, pendingRequired, ready, }; }; const describeGating = (stats: ReleaseChecklistStats): string => { if (stats.ready) { return "All checks complete"; } const segments: string[] = []; if (stats.blocked.length > 0) { segments.push(`Blocked: ${stats.blocked.join(", ")}`); } if (stats.pendingRequired.length > 0) { segments.push(`Pending: ${stats.pendingRequired.join(", ")}`); } return segments.join(" | ") || "Pending"; }; const updateTaskProgress = handler( ( event: TaskProgressEvent | undefined, context: { tasks: Cell }, ) => { const id = typeof event?.id === "string" ? event.id.trim() : ""; if (id.length === 0) return; const tasks = ensureTaskList(context.tasks); const index = tasks.findIndex((task) => task.id === id); if (index === -1) return; const status = toStatus(event?.status) ?? undefined; const ownerValue = event?.owner; const noteValue = event?.note; const owner = ownerValue === undefined ? undefined : toOptionalString(ownerValue); const note = noteValue === undefined ? undefined : toOptionalString(noteValue); const next = tasks.slice(); const current = next[index]; next[index] = { id: current.id, label: current.label, required: current.required, status: status ?? current.status, owner: owner === undefined ? current.owner : owner, note: note === undefined ? current.note : note, }; context.tasks.set(next); }, ); export const releaseChecklist = recipe( "Release Checklist", ({ tasks }) => { const sanitizedTasks = lift(sanitizeTaskList)(tasks); const stats = lift(analyzeTasks)(sanitizedTasks); const readyFlag = lift((info: ReleaseChecklistStats) => info.ready)(stats); const status = lift((info: ReleaseChecklistStats) => { if (info.ready) return "ready"; if (info.blocked.length > 0) return "blocked"; return "pending"; })(stats); const statusCaps = lift((value: string) => value.toUpperCase())(status); const requiredTotal = lift((info: ReleaseChecklistStats) => info.requiredTotal )(stats); const completedRequired = lift((info: ReleaseChecklistStats) => info.completedRequired )(stats); const completedTotal = lift((info: ReleaseChecklistStats) => info.completedTotal )(stats); const totalTasks = lift((info: ReleaseChecklistStats) => info.total)(stats); const blockedCount = lift((info: ReleaseChecklistStats) => info.blocked.length )(stats); const remainingRequired = lift((info: ReleaseChecklistStats) => info.pendingRequired )(stats); const blockedTasks = lift((info: ReleaseChecklistStats) => info.blocked)( stats, ); const gatingNote = lift(describeGating)(stats); const summary = str`${completedRequired}/${requiredTotal} required complete`; const headline = str`${statusCaps} • ${completedTotal}/${totalTasks} tasks done`; return { tasks: sanitizedTasks, ready: readyFlag, status, headline, summary, gatingNote, requiredTotal, completedRequired, completedTotal, blockedCount, remainingRequired, blocked: blockedTasks, updateTask: updateTaskProgress({ tasks }), }; }, ); export type { ReleaseTask };