diff --git a/src/dotnet/QuantEngine.Application/Services/DecisionLearningService.cs b/src/dotnet/QuantEngine.Application/Services/DecisionLearningService.cs
new file mode 100644
index 00000000..926e0526
--- /dev/null
+++ b/src/dotnet/QuantEngine.Application/Services/DecisionLearningService.cs
@@ -0,0 +1,101 @@
+using System.Text.Json;
+using QuantEngine.Core.Interfaces;
+
+namespace QuantEngine.Application.Services;
+
+///
+/// Canonical application path for recording factor evidence, decisions, and
+/// realized outcomes in the normalized PostgreSQL learning store.
+///
+public sealed class DecisionLearningService
+{
+ private readonly INormalizedLearningStore _store;
+
+ public DecisionLearningService(INormalizedLearningStore store) => _store = store;
+
+ public async Task RecordDecisionAsync(
+ string decisionKey,
+ DateTimeOffset decidedAt,
+ string instrumentId,
+ string action,
+ string gate,
+ decimal? score,
+ string sourceVersion,
+ IEnumerable factors,
+ object? trace = null,
+ object? provenance = null)
+ {
+ var decisionId = await _store.AppendDecisionAsync(new DecisionEventRecord(
+ decisionKey,
+ decidedAt,
+ instrumentId,
+ action,
+ gate,
+ score,
+ sourceVersion,
+ JsonSerializer.Serialize(trace ?? new { }),
+ JsonSerializer.Serialize(provenance ?? new { })));
+
+ foreach (var factor in factors)
+ {
+ var observationId = await _store.AppendSourceObservationAsync(new SourceObservationRecord(
+ factor.ObservedAt,
+ instrumentId,
+ factor.SourceName,
+ sourceVersion,
+ factor.PayloadJson,
+ factor.ProvenanceJson));
+ var factorObservationId = await _store.AppendFactorObservationAsync(new FactorObservationRecord(
+ observationId,
+ factor.FactorObservationId,
+ factor.FactorId,
+ factor.FactorVersion,
+ factor.ObservedAt,
+ factor.NumericValue,
+ factor.TextValue,
+ factor.Gate,
+ factor.ProvenanceJson));
+ await _store.AppendDecisionFactorEvidenceAsync(decisionId, factorObservationId, factor.Role);
+ }
+
+ return decisionId;
+ }
+
+ public Task RecordOutcomeAsync(
+ Guid decisionId,
+ int horizonDays,
+ DateTimeOffset evaluatedAt,
+ decimal? realizedReturn,
+ decimal? benchmarkReturn,
+ string outcomeClass,
+ string evaluationGate,
+ object? provenance = null)
+ {
+ decimal? excessReturn = realizedReturn.HasValue && benchmarkReturn.HasValue
+ ? realizedReturn.Value - benchmarkReturn.Value
+ : null;
+ return _store.AppendOutcomeAsync(new OutcomeEvaluationRecord(
+ decisionId,
+ horizonDays,
+ evaluatedAt,
+ realizedReturn,
+ benchmarkReturn,
+ excessReturn,
+ outcomeClass,
+ evaluationGate,
+ JsonSerializer.Serialize(provenance ?? new { })));
+ }
+}
+
+public sealed record FactorEvidenceInput(
+ Guid FactorObservationId,
+ string FactorId,
+ string FactorVersion,
+ DateTimeOffset ObservedAt,
+ decimal? NumericValue,
+ string? TextValue,
+ string Gate,
+ string Role,
+ string SourceName,
+ string PayloadJson,
+ string ProvenanceJson);
diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs
index 194bdd57..19de1372 100644
--- a/src/dotnet/QuantEngine.Web/Program.cs
+++ b/src/dotnet/QuantEngine.Web/Program.cs
@@ -97,6 +97,7 @@ try
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();