using System.Text.RegularExpressions; namespace ItWorx.KnowledgeHub.Domain.Sources; public enum DataSourceKind { Inventory, Gitea, KnowledgeClient, ManualIntake, LeafWiki, CuratedPackage } /// Operator registration and observed evidence; credentials never enter this aggregate. public sealed class DataSource { private DataSource() { } public Guid Id { get; private set; } public Guid WorkspaceId { get; private set; } public string SourceKey { get; private set; } = ""; public string DisplayName { get; private set; } = ""; public DataSourceKind Kind { get; private set; } public bool Enabled { get; private set; } = true; public int ExpectedIntervalHours { get; private set; } public int Version { get; private set; } = 1; public DateTimeOffset CreatedAt { get; private set; } public DateTimeOffset? LastAttemptAt { get; private set; } public DateTimeOffset? LastSuccessAt { get; private set; } public DateTimeOffset? LastFailureAt { get; private set; } public DateTimeOffset? DataObservedAt { get; private set; } public long? RecordCount { get; private set; } public string? LastErrorCode { get; private set; } public string? LastCorrelationId { get; private set; } public static DataSource Create(Guid workspaceId, string sourceKey, string displayName, DataSourceKind kind, int expectedIntervalHours, DateTimeOffset now) { if (workspaceId == Guid.Empty || !Enum.IsDefined(kind) || !Regex.IsMatch(sourceKey, "^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,199}$", RegexOptions.CultureInvariant)) { throw new ArgumentException("Invalid source identity."); } var source = new DataSource { Id = Guid.NewGuid(), WorkspaceId = workspaceId, SourceKey = sourceKey, Kind = kind, CreatedAt = now, Version = 0 }; source.Configure(displayName, true, expectedIntervalHours); return source; } public void Configure(string displayName, bool enabled, int expectedIntervalHours) { if (string.IsNullOrWhiteSpace(displayName) || displayName.Length > 120 || displayName.Any(char.IsControl) || expectedIntervalHours is < 1 or > 8760) { throw new ArgumentException("Invalid source policy."); } DisplayName = displayName.Trim(); Enabled = enabled; ExpectedIntervalHours = expectedIntervalHours; Version++; } public void Observe(bool success, long? recordCount, string? errorCode, string? correlationId, DateTimeOffset observedAt, DateTimeOffset? dataObservedAt = null) { if (recordCount < 0 || (!success && errorCode is not ("authentication" or "connectivity" or "validation" or "unexpected")) || (LastAttemptAt is { } previous && observedAt < previous) || dataObservedAt > observedAt || correlationId?.Length > 100 || correlationId?.Any(char.IsControl) == true) { throw new ArgumentException("Invalid source observation."); } LastAttemptAt = observedAt; LastCorrelationId = correlationId; if (success) { LastSuccessAt = observedAt; var captured = dataObservedAt ?? observedAt; if (DataObservedAt is null || captured >= DataObservedAt) { DataObservedAt = captured; RecordCount = recordCount; } LastErrorCode = null; } else { LastFailureAt = observedAt; LastErrorCode = errorCode; } Version++; } public string GetState(DateTimeOffset now) { if (!Enabled) return "Disabled"; if (LastErrorCode is not null) return "Failed"; if (DataObservedAt is null || DataObservedAt > now) return "Unknown"; return now - DataObservedAt.Value > TimeSpan.FromHours(ExpectedIntervalHours) ? "Stale" : "Current"; } }