/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; const statusOrder = [ "pending", "in_progress", "complete", "waived", ] as const; type ComplianceStatus = typeof statusOrder[number]; type ComplianceState = "compliant" | "at_risk" | "non_compliant"; const statusSet = new Set(statusOrder); const statusLabels: Record = { pending: "Pending", in_progress: "In Progress", complete: "Complete", waived: "Waived", }; const complianceLabels: Record = { compliant: "Compliant", at_risk: "At Risk", non_compliant: "Non-Compliant", }; interface ChecklistTaskInput { id?: string; taskId?: string; reference?: string; label?: string; category?: string; mandatory?: boolean; status?: string; owner?: string | null; evidence?: string | null; note?: string | null; state?: string | null; } interface ComplianceTask { id: string; label: string; category: string; mandatory: boolean; status: ComplianceStatus; owner: string | null; evidence: string | null; } interface CategorySummary { category: string; total: number; mandatory: number; satisfied: number; outstanding: number; coverage: number; label: string; } interface ComplianceGap { id: string; label: string; category: string; owner: string | null; status: ComplianceStatus; mandatory: boolean; } interface ComplianceChecklistArgs { tasks: Default; } interface TaskProgressEvent { id?: string; taskId?: string; reference?: string; status?: string; state?: string; owner?: string | null; evidence?: string | null; note?: string | null; } interface ComplianceInsights { coveragePercent: number; mandatoryTotal: number; mandatorySatisfied: number; gapList: ComplianceGap[]; categories: CategorySummary[]; status: ComplianceState; } interface CategoryAccumulator { category: string; total: number; mandatory: number; satisfied: number; outstanding: number; } type TaskOverrideMap = Record; const defaultTasks: ComplianceTask[] = [ { id: "data-retention", label: "Data Retention Policy", category: "Policy", mandatory: true, status: "complete", owner: "Morgan Patel", evidence: "Executive approval logged", }, { id: "security-awareness", label: "Security Awareness Training", category: "Training", mandatory: true, status: "pending", owner: null, evidence: null, }, { id: "access-review", label: "Access Review Audit", category: "Audit", mandatory: true, status: "in_progress", owner: "Jordan Lee", evidence: null, }, { id: "vendor-assessment", label: "Vendor Risk Assessment", category: "Third-Party", mandatory: false, status: "pending", owner: null, evidence: null, }, ]; const cloneTask = (task: ComplianceTask): ComplianceTask => ({ id: task.id, label: task.label, category: task.category, mandatory: task.mandatory, status: task.status, owner: task.owner ?? null, evidence: task.evidence ?? null, }); const cloneTasks = (entries: readonly ComplianceTask[]): ComplianceTask[] => entries.map((entry) => cloneTask(entry)); const compareTasks = (left: ComplianceTask, right: ComplianceTask): number => { const categoryCompare = left.category.localeCompare(right.category); if (categoryCompare !== 0) return categoryCompare; if (left.mandatory !== right.mandatory) { return left.mandatory ? -1 : 1; } const labelCompare = left.label.localeCompare(right.label); if (labelCompare !== 0) return labelCompare; return left.id.localeCompare(right.id); }; const sanitizeTaskList = (value: unknown): ComplianceTask[] => { if (!Array.isArray(value)) { return cloneTasks(defaultTasks); } const seen = new Set(); const sanitized: ComplianceTask[] = []; for (const raw of value) { const task = sanitizeTask(raw); 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 sanitizeTask = (value: unknown): ComplianceTask | null => { const input = value as ChecklistTaskInput | undefined; const id = resolveSanitizedId(input); if (!id) return null; const label = sanitizeLabel(input?.label, id); const category = sanitizeCategory(input?.category); const mandatory = typeof input?.mandatory === "boolean" ? input.mandatory : true; const status = sanitizeStatus(input?.status ?? input?.state, "pending"); const owner = sanitizeOwnerInput(input?.owner); const evidence = sanitizeEvidence(input?.evidence ?? input?.note); return { id, label, category, mandatory, status, owner, evidence }; }; const resolveSanitizedId = ( input: ChecklistTaskInput | undefined, ): string | null => { const candidates = [input?.id, input?.taskId, input?.reference]; for (const candidate of candidates) { const id = sanitizeTaskId(candidate); if (id) return id; } return null; }; const sanitizeTaskId = (value: unknown): string | null => { if (typeof value !== "string") return null; const trimmed = value.trim().toLowerCase(); if (!trimmed) return null; const normalized = trimmed .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return normalized ? normalized : null; }; const formatLabelFromId = (id: string): string => id.split(/[-_]/).map(normalizeWord).join(" "); const normalizeWord = (value: string): string => { if (!value) return value; // Handle hyphenated compound words like "Third-Party" if (value.includes("-")) { return value.split("-").map((part) => { const lower = part.toLowerCase(); return lower.slice(0, 1).toUpperCase() + lower.slice(1); }).join("-"); } const lower = value.toLowerCase(); return lower.slice(0, 1).toUpperCase() + lower.slice(1); }; const normalizeWords = (value: string): string => value.split(/[\s_]+/).filter(Boolean).map(normalizeWord).join(" "); const sanitizeLabel = (value: unknown, fallbackId: string): string => { if (typeof value !== "string") { return formatLabelFromId(fallbackId); } const trimmed = value.trim(); if (!trimmed) { return formatLabelFromId(fallbackId); } return normalizeWords(trimmed); }; const sanitizeCategory = (value: unknown): string => { if (typeof value !== "string") return "General"; const trimmed = value.trim(); if (!trimmed) return "General"; return normalizeWords(trimmed); }; const sanitizeOwnerInput = (value: unknown): string | null => { if (typeof value !== "string") return null; const trimmed = value.trim(); if (!trimmed) return null; return normalizeWords(trimmed); }; const sanitizeEvidence = (value: unknown): string | null => { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed ? trimmed : null; }; const sanitizeStatus = ( value: unknown, fallback: ComplianceStatus, ): ComplianceStatus => { if (typeof value !== "string") return fallback; const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_"); if (normalized === "inprogress") { return "in_progress"; } if (statusSet.has(normalized as ComplianceStatus)) { return normalized as ComplianceStatus; } return fallback; }; const isSatisfied = (status: ComplianceStatus): boolean => status === "complete" || status === "waived"; const formatCategoryLabel = ( category: string, bucket: CategoryAccumulator, ): string => { if (bucket.mandatory === 0) { return `${category}: no mandatory tasks`; } const coverageNote = `${bucket.satisfied}/${bucket.mandatory} mandatory complete`; if (bucket.outstanding === 0) { return `${category}: ${coverageNote}`; } return `${category}: ${coverageNote} (${bucket.outstanding} outstanding)`; }; const formatStatusLabel = (status: ComplianceStatus): string => statusLabels[status]; const formatStateLabel = (state: ComplianceState): string => complianceLabels[state]; const computeInsights = ( tasks: readonly ComplianceTask[], ): ComplianceInsights => { const categories = new Map(); const gaps: ComplianceGap[] = []; let mandatoryTotal = 0; let mandatorySatisfied = 0; for (const task of tasks) { const bucket = categories.get(task.category) ?? { category: task.category, total: 0, mandatory: 0, satisfied: 0, outstanding: 0, }; bucket.total += 1; if (task.mandatory) { bucket.mandatory += 1; mandatoryTotal += 1; if (isSatisfied(task.status)) { bucket.satisfied += 1; mandatorySatisfied += 1; } else { bucket.outstanding += 1; gaps.push({ id: task.id, label: task.label, category: task.category, owner: task.owner, status: task.status, mandatory: true, }); } } categories.set(task.category, bucket); } const categorySummaries = Array.from(categories.values()).map((bucket) => { const coverage = bucket.mandatory === 0 ? 100 : Math.round((bucket.satisfied / bucket.mandatory) * 100); return { category: bucket.category, total: bucket.total, mandatory: bucket.mandatory, satisfied: bucket.satisfied, outstanding: bucket.outstanding, coverage, label: formatCategoryLabel(bucket.category, bucket), } satisfies CategorySummary; }); categorySummaries.sort((left, right) => left.category.localeCompare(right.category) ); const sortedGaps = [...gaps].sort((left, right) => { const categoryCompare = left.category.localeCompare(right.category); if (categoryCompare !== 0) return categoryCompare; const labelCompare = left.label.localeCompare(right.label); if (labelCompare !== 0) return labelCompare; return left.id.localeCompare(right.id); }); const coveragePercent = mandatoryTotal === 0 ? 100 : Math.round((mandatorySatisfied / mandatoryTotal) * 100); const status: ComplianceState = coveragePercent === 100 ? "compliant" : coveragePercent >= 60 ? "at_risk" : "non_compliant"; return { coveragePercent, mandatoryTotal, mandatorySatisfied, gapList: sortedGaps, categories: categorySummaries, status, }; }; const cloneOverrideMap = (value: unknown): TaskOverrideMap => { if (!value || typeof value !== "object") return {}; const result: TaskOverrideMap = {}; for (const [key, task] of Object.entries(value as TaskOverrideMap)) { if (!task || typeof task !== "object") continue; result[key] = cloneTask(task as ComplianceTask); } return result; }; const mergeTasks = ( defaults: readonly ComplianceTask[], overrides: TaskOverrideMap, ): ComplianceTask[] => { const merged = defaults.map((task) => { const override = overrides[task.id]; return override ? cloneTask(override) : cloneTask(task); }); for (const [id, override] of Object.entries(overrides)) { if (!merged.some((task) => task.id === id)) { merged.push(cloneTask(override)); } } merged.sort(compareTasks); return merged; }; const tasksEqual = (left: ComplianceTask, right: ComplianceTask): boolean => left.id === right.id && left.label === right.label && left.category === right.category && left.mandatory === right.mandatory && left.status === right.status && left.owner === right.owner && left.evidence === right.evidence; const hasOwn = ( value: TaskProgressEvent | undefined, key: keyof TaskProgressEvent, ): boolean => value !== undefined && Object.prototype.hasOwnProperty.call(value, key); const resolveTaskId = (event: TaskProgressEvent | undefined): string | null => { if (!event) return null; const candidates = [event.id, event.taskId, event.reference]; for (const candidate of candidates) { const id = sanitizeTaskId(candidate); if (id) return id; } return null; }; const updateComplianceTask = handler( ( event: TaskProgressEvent | undefined, context: { defaults: Cell; overrides: Cell; history: Cell; }, ) => { const id = resolveTaskId(event); if (!id) return; const baseList = context.defaults.get(); if (!Array.isArray(baseList) || baseList.length === 0) return; const defaults = baseList.map(cloneTask); const overrides = cloneOverrideMap(context.overrides.get()); const current = mergeTasks(defaults, overrides); const index = current.findIndex((task) => task.id === id); if (index === -1) return; const previous = { ...current[index] }; const statusValue = sanitizeStatus( event?.status ?? event?.state, previous.status, ); const ownerProvided = hasOwn(event, "owner"); const nextOwner = ownerProvided ? sanitizeOwnerInput(event?.owner) : previous.owner; const evidenceProvided = hasOwn(event, "evidence") || hasOwn(event, "note"); const nextEvidence = evidenceProvided ? sanitizeEvidence( hasOwn(event, "evidence") ? event?.evidence : event?.note, ) : previous.evidence; let didChange = false; const nextTask: ComplianceTask = { ...previous }; if (statusValue !== previous.status) { nextTask.status = statusValue; didChange = true; } if (ownerProvided && nextOwner !== previous.owner) { nextTask.owner = nextOwner; didChange = true; } if (evidenceProvided && nextEvidence !== previous.evidence) { nextTask.evidence = nextEvidence; didChange = true; } if (!didChange) return; current[index] = nextTask; current.sort(compareTasks); const baseMap = new Map(defaults.map((task) => [task.id, task])); const baseTask = baseMap.get(id); const nextOverrides: TaskOverrideMap = { ...overrides }; if (baseTask && tasksEqual(baseTask, nextTask)) { delete nextOverrides[id]; } else { nextOverrides[id] = cloneTask(nextTask); } context.overrides.set(nextOverrides); const historyEntry = buildHistoryEntry(nextTask, { statusChanged: nextTask.status !== previous.status, ownerChanged: nextTask.owner !== previous.owner, evidenceChanged: nextTask.evidence !== previous.evidence, }); const history = context.history.get(); const nextHistory = Array.isArray(history) ? [...history, historyEntry] : [historyEntry]; context.history.set(nextHistory); }, ); const buildHistoryEntry = ( task: ComplianceTask, flags: { statusChanged: boolean; ownerChanged: boolean; evidenceChanged: boolean; }, ): string => { const segments: string[] = []; if (flags.statusChanged) { segments.push(`status ${formatStatusLabel(task.status)}`); } if (flags.ownerChanged) { segments.push(task.owner ? `owner ${task.owner}` : "owner cleared"); } if (flags.evidenceChanged) { segments.push(task.evidence ? "evidence recorded" : "evidence cleared"); } const detail = segments.length > 0 ? segments.join(" | ") : "updated"; return `${task.label}: ${detail}`; }; export const complianceChecklist = recipe( "Compliance Checklist", ({ tasks }) => { const canonicalDefaults = lift(sanitizeTaskList)(tasks); const overrideStore = cell({}); const auditStore = cell([]); const currentTasks = lift((input: { defaults: ComplianceTask[]; overrides: TaskOverrideMap; }) => mergeTasks(input.defaults, input.overrides))({ defaults: canonicalDefaults, overrides: overrideStore, }); const insights = lift(computeInsights)(currentTasks); const tasksView = lift(cloneTasks)(currentTasks); const categorySummaries = insights.categories; const gapDetails = insights.gapList; const coveragePercent = lift((snapshot: ComplianceInsights) => snapshot.coveragePercent )(insights); const gapCount = lift((snapshot: ComplianceInsights) => snapshot.gapList.length )(insights); const gapWord = lift((count: number) => (count === 1 ? "gap" : "gaps"))( gapCount, ); const complianceState = lift((snapshot: ComplianceInsights) => formatStateLabel(snapshot.status) )(insights); const statusLabel = str`${coveragePercent}% coverage (${complianceState}) with ${gapCount} ${gapWord}`; const mandatorySummary = lift((snapshot: ComplianceInsights) => ({ total: snapshot.mandatoryTotal, satisfied: snapshot.mandatorySatisfied, }))(insights); const auditTrail = lift((entries: string[] | undefined) => Array.isArray(entries) ? entries : [] )(auditStore); return { tasks: tasksView, categories: categorySummaries, coveragePercent, gapCount, complianceState, statusLabel, gapTasks: gapDetails, mandatorySummary, auditTrail, updateTask: updateComplianceTask({ defaults: canonicalDefaults, overrides: overrideStore, history: auditStore, }), }; }, ); export type { CategorySummary, ChecklistTaskInput, ComplianceChecklistArgs, ComplianceGap, ComplianceInsights, ComplianceState, ComplianceStatus, ComplianceTask, TaskProgressEvent, };