88ea5edef5
Source: CLAUDE.md observability section - Serilog structured logging and performance are first-class concerns Slice: ModelOperations/Scheduling, Domain/ModelFeedbackCycle Policy: CA1859 (concrete types over interfaces), CA1848 (LoggerMessage delegates) Changes: - ScheduledModelOperationJob: LoggerMessage.Define for warning/info logs - ModelOperationsDispatcherJob: LoggerMessage.Define for error logs - ModelOperationExecution: Dictionary<State, HashSet<State>> state machine - ModelFeedbackCycle: Dictionary<State, HashSet<State>> state machine Verification: - dotnet build: 0 errors, 0 warnings - dotnet test: 41/41 tests passed (ArchitectureTests 5, ModelOperations 17, SignalEngine 18) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
95 lines
4.7 KiB
C#
95 lines
4.7 KiB
C#
namespace KArtSell.Modules.ModelOperations.FeedbackLoop;
|
|
|
|
public enum ModelFeedbackCycleState
|
|
{
|
|
Planned,
|
|
PredictionFrozen,
|
|
OutcomesMaturing,
|
|
Evaluated,
|
|
ImprovementProposed,
|
|
ChallengerPlanned,
|
|
IndependentlyValidated,
|
|
PromotionReviewPending,
|
|
BusinessHold,
|
|
Closed
|
|
}
|
|
|
|
public sealed record ModelFeedbackTransition(
|
|
ModelFeedbackCycleState From,
|
|
ModelFeedbackCycleState To,
|
|
string ReasonCode,
|
|
string EvidenceHash,
|
|
string ActorId,
|
|
DateTimeOffset OccurredAt);
|
|
|
|
public sealed class ModelFeedbackCycle
|
|
{
|
|
private static readonly Dictionary<ModelFeedbackCycleState, HashSet<ModelFeedbackCycleState>> Allowed =
|
|
new()
|
|
{
|
|
[ModelFeedbackCycleState.Planned] = new(new[] { ModelFeedbackCycleState.PredictionFrozen, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.PredictionFrozen] = new(new[] { ModelFeedbackCycleState.OutcomesMaturing, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.OutcomesMaturing] = new(new[] { ModelFeedbackCycleState.Evaluated, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.Evaluated] = new(new[] { ModelFeedbackCycleState.ImprovementProposed, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.ImprovementProposed] = new(new[] { ModelFeedbackCycleState.ChallengerPlanned, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.ChallengerPlanned] = new(new[] { ModelFeedbackCycleState.IndependentlyValidated, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.IndependentlyValidated] = new(new[] { ModelFeedbackCycleState.PromotionReviewPending, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.PromotionReviewPending] = new(new[] { ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }),
|
|
[ModelFeedbackCycleState.BusinessHold] = new(new[] { ModelFeedbackCycleState.Planned, ModelFeedbackCycleState.Closed }),
|
|
[ModelFeedbackCycleState.Closed] = new()
|
|
};
|
|
|
|
private readonly List<ModelFeedbackTransition> transitions = [];
|
|
|
|
public ModelFeedbackCycle(Guid cycleId, string scopeKey, string baseModelVersion, DateTimeOffset startedAt)
|
|
{
|
|
if (cycleId == Guid.Empty) throw new ArgumentException("Cycle ID is required.", nameof(cycleId));
|
|
if (string.IsNullOrWhiteSpace(scopeKey)) throw new ArgumentException("Scope key is required.", nameof(scopeKey));
|
|
if (string.IsNullOrWhiteSpace(baseModelVersion)) throw new ArgumentException("Base model version is required.", nameof(baseModelVersion));
|
|
|
|
CycleId = cycleId;
|
|
ScopeKey = scopeKey;
|
|
BaseModelVersion = baseModelVersion;
|
|
StartedAt = startedAt;
|
|
State = ModelFeedbackCycleState.Planned;
|
|
Revision = 1;
|
|
LastOccurredAt = startedAt;
|
|
}
|
|
|
|
public Guid CycleId { get; }
|
|
public string ScopeKey { get; }
|
|
public string BaseModelVersion { get; }
|
|
public DateTimeOffset StartedAt { get; }
|
|
public ModelFeedbackCycleState State { get; private set; }
|
|
public int Revision { get; private set; }
|
|
public DateTimeOffset LastOccurredAt { get; private set; }
|
|
public DateTimeOffset? ClosedAt { get; private set; }
|
|
public IReadOnlyList<ModelFeedbackTransition> Transitions => transitions;
|
|
|
|
public ModelFeedbackTransition MoveTo(
|
|
ModelFeedbackCycleState next,
|
|
string reasonCode,
|
|
string evidenceHash,
|
|
string actorId,
|
|
DateTimeOffset occurredAt)
|
|
{
|
|
if (!Allowed[State].Contains(next))
|
|
throw new InvalidOperationException($"Feedback cycle transition {State} -> {next} is not allowed.");
|
|
if (string.IsNullOrWhiteSpace(reasonCode)) throw new ArgumentException("Reason code is required.", nameof(reasonCode));
|
|
if (string.IsNullOrWhiteSpace(evidenceHash)) throw new ArgumentException("Evidence hash is required.", nameof(evidenceHash));
|
|
if (string.IsNullOrWhiteSpace(actorId)) throw new ArgumentException("Actor ID is required.", nameof(actorId));
|
|
if (occurredAt < LastOccurredAt) throw new InvalidOperationException("Feedback transition time cannot move backwards.");
|
|
|
|
var transition = new ModelFeedbackTransition(State, next, reasonCode, evidenceHash, actorId, occurredAt);
|
|
transitions.Add(transition);
|
|
State = next;
|
|
Revision++;
|
|
LastOccurredAt = occurredAt;
|
|
if (next == ModelFeedbackCycleState.Closed) ClosedAt = occurredAt;
|
|
return transition;
|
|
}
|
|
|
|
private static HashSet<ModelFeedbackCycleState> Set(params ModelFeedbackCycleState[] states)
|
|
=> new(states);
|
|
}
|