namespace Nexus.Api.Data; public enum OperationalStatus { Online, Degraded, Offline, Unknown } /// /// Strongly-typed task lifecycle states. /// String values (e.g. "In progress") are preserved for API compatibility /// via ; the WorkTask entity continues to store /// state as a string in the database. /// public enum TaskState { Backlog, InProgress, Delegated, Blocked, Done, Review } public static class TaskStateHelper { private static readonly Dictionary StateToString = new() { [TaskState.Backlog] = "Backlog", [TaskState.InProgress] = "In progress", [TaskState.Delegated] = "Delegated", [TaskState.Blocked] = "Blocked", [TaskState.Done] = "Done", [TaskState.Review] = "Review" }; private static readonly Dictionary StringToState = new(StringComparer.OrdinalIgnoreCase) { ["Backlog"] = TaskState.Backlog, ["In progress"] = TaskState.InProgress, ["Delegated"] = TaskState.Delegated, ["Blocked"] = TaskState.Blocked, ["Done"] = TaskState.Done, ["Review"] = TaskState.Review }; /// Mapping from state string to display label. private static readonly Dictionary DisplayLabels = new(StringComparer.OrdinalIgnoreCase) { ["Backlog"] = "Offen", ["In progress"] = "In Bearbeitung", ["Delegated"] = "Delegiert", ["Review"] = "Review", ["Blocked"] = "Blockiert", ["Done"] = "Erledigt" }; /// Valid task-state string values for API validation. public static readonly string[] AllStates = ["Backlog", "In progress", "Delegated", "Blocked", "Done", "Review"]; /// Convert a TaskState enum to its API string representation. public static string ToStateString(this TaskState state) => StateToString[state]; /// Parse a string to TaskState; defaults to Backlog for unrecognized input. public static TaskState ToTaskState(this string state) => StringToState.TryGetValue(state, out var result) ? result : TaskState.Backlog; /// Returns true if the string is a recognized task state (case-insensitive). public static bool IsValidState(string? state) => !string.IsNullOrWhiteSpace(state) && StringToState.ContainsKey(state); /// Returns the German display label for a state string. public static string ToDisplayString(string? state) => state is not null && DisplayLabels.TryGetValue(state, out var label) ? label : state ?? ""; public static bool IsInProgressOrBlocked(string? state) => string.Equals(state, "In progress", StringComparison.OrdinalIgnoreCase) || string.Equals(state, "Blocked", StringComparison.OrdinalIgnoreCase); public static bool IsDoneOrBacklog(string? state) => string.Equals(state, "Done", StringComparison.OrdinalIgnoreCase) || string.Equals(state, "Backlog", StringComparison.OrdinalIgnoreCase); /// /// Returns true if the caller is allowed to change this task's state. /// POLICY: /// - **Iris und Bao** dürfen Status ändern / verschieben. /// - Sub-agents (programmer, reviewer, architekt) dürfen NIEMALS Status ändern. /// - 'nexus-system' ist ein technischer Fallback für automatische Cron/Reset-Workflows. /// - Jeder andere (unbekannt, leer) wird abgewiesen. /// public static bool CanChangeState(string? callerAgent, WorkTask task) { var caller = callerAgent?.Trim().ToLowerInvariant() ?? ""; // Sub-agents must never move state var subAgents = new HashSet { "programmer", "reviewer", "architekt" }; if (subAgents.Contains(caller)) return false; // Technischer Fallback: nur für interne System-Operationen (Cron, ResetStale) if (caller == "nexus-system") return true; // Iris und Bao dürfen Status ändern return caller == "iris" || caller == "bao"; } /// /// Returns true if the caller is allowed to edit a task's content fields /// (title, detail, priority, assignedTo, dueDate). /// POLICY: /// - Alle (iris, bao, sub-agents, nexus-system) dürfen inhaltlich bearbeiten. /// - Nur unbekannte/leere Caller werden abgewiesen. /// public static bool CanEditContent(string? callerAgent) { var caller = callerAgent?.Trim().ToLowerInvariant() ?? ""; if (string.IsNullOrWhiteSpace(caller)) return false; return true; } /// Group key for board responses (lowercased English state). public static string BoardGroupKey(string? state) { if (string.IsNullOrWhiteSpace(state)) return "offen"; var lower = state.ToLowerInvariant(); return lower switch { "backlog" => "offen", "in progress" => "inProgress", "delegated" => "delegated", "review" => "review", "blocked" => "blocked", "done" => "done", _ => "offen" }; } /// Map a board group key back to the canonical state string. public static string? BoardGroupToState(string? groupKey) { if (string.IsNullOrWhiteSpace(groupKey)) return null; var lower = groupKey.ToLowerInvariant(); return lower switch { "offen" => "Backlog", "inprogress" => "In progress", "delegated" => "Delegated", "review" => "Review", "blocked" => "Blocked", "done" => "Done", _ => null }; } } public sealed class Project { public Guid Id { get; init; } = Guid.NewGuid(); public required string Name { get; set; } public string Description { get; set; } = string.Empty; public int Progress { get; set; } public OperationalStatus Status { get; set; } = OperationalStatus.Unknown; public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; } public sealed class WorkTask { public Guid Id { get; init; } = Guid.NewGuid(); public required string Title { get; set; } public string? Detail { get; set; } public string State { get; set; } = "Backlog"; public string Priority { get; set; } = "Normal"; public string Source { get; set; } = "bao"; public string? AssignedTo { get; set; } /// /// True if this task was created programmatically by an agent (not manually by Bao). /// Agent-tasks in the board are subject to stricter workflow rules. /// public bool IsAgentTask { get; set; } = false; /// /// Which agent/user is expected to respond next. /// Helps Iris see who she is waiting for. /// public string? ExpectedFrom { get; set; } public Guid? ParentTaskId { get; set; } public WorkTask? ParentTask { get; set; } public ICollection ChildTasks { get; set; } = new List(); public Guid? ProjectId { get; set; } public DateTimeOffset? DueDate { get; set; } public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; } public sealed class Notification { public Guid Id { get; init; } = Guid.NewGuid(); public required string Type { get; set; } // "task_assigned", "task_review", "task_blocked" public required string Title { get; set; } // "Neue Aufgabe: Memory-Index reparieren" public string? Message { get; set; } // Detailtext public required string ForUser { get; set; } // "bao" oder "iris" public Guid? TaskId { get; set; } // Verknüpfte Task public bool IsRead { get; set; } = false; public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; } public sealed class ActivityEvent { public long Id { get; init; } public required string Type { get; set; } public required string Message { get; set; } public Guid? TaskId { get; set; } public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; }