using Dapper; using QuantEngine.Core.Interfaces; using QuantEngine.Infrastructure.Data; namespace QuantEngine.Infrastructure.Repositories; public sealed class NormalizedLearningStore : INormalizedLearningStore { private readonly IDbConnectionFactory _connectionFactory; public NormalizedLearningStore(IDbConnectionFactory connectionFactory) => _connectionFactory = connectionFactory; public async Task AppendSourceObservationAsync(SourceObservationRecord record) { var id = Guid.NewGuid(); using var conn = _connectionFactory.CreateConnection(); await conn.ExecuteAsync(@"INSERT INTO engine_history.source_observation (observation_id, observed_at, instrument_id, source_name, source_version, payload, provenance) VALUES (@Id, @ObservedAt, @InstrumentId, @SourceName, @SourceVersion, CAST(@PayloadJson AS jsonb), CAST(@ProvenanceJson AS jsonb))", new { Id = id, record.ObservedAt, record.InstrumentId, record.SourceName, record.SourceVersion, record.PayloadJson, record.ProvenanceJson }); return id; } public async Task AppendFactorObservationAsync(FactorObservationRecord record) { using var conn = _connectionFactory.CreateConnection(); await conn.ExecuteAsync(@"INSERT INTO engine_history.factor_observation (factor_observation_id, observation_id, factor_id, factor_version, observed_at, numeric_value, text_value, gate, provenance) VALUES (@FactorObservationId, @ObservationId, @FactorId, @FactorVersion, @ObservedAt, @NumericValue, @TextValue, @Gate, CAST(@ProvenanceJson AS jsonb))", new { record.FactorObservationId, record.ObservationId, record.FactorId, record.FactorVersion, record.ObservedAt, record.NumericValue, record.TextValue, record.Gate, record.ProvenanceJson }); return record.FactorObservationId; } public async Task AppendDecisionAsync(DecisionEventRecord record) { var id = Guid.NewGuid(); using var conn = _connectionFactory.CreateConnection(); await conn.ExecuteAsync(@"INSERT INTO engine_history.decision_event (decision_id, decision_key, decided_at, instrument_id, action, gate, score, source_version, trace, provenance) VALUES (@Id, @DecisionKey, @DecidedAt, @InstrumentId, @Action, @Gate, @Score, @SourceVersion, CAST(@TraceJson AS jsonb), CAST(@ProvenanceJson AS jsonb))", new { Id = id, record.DecisionKey, record.DecidedAt, record.InstrumentId, record.Action, record.Gate, record.Score, record.SourceVersion, record.TraceJson, record.ProvenanceJson }); return id; } public async Task AppendDecisionFactorEvidenceAsync(Guid decisionId, Guid factorObservationId, string role) { using var conn = _connectionFactory.CreateConnection(); await conn.ExecuteAsync(@"INSERT INTO engine_history.decision_factor_evidence (decision_id, factor_observation_id, role) VALUES (@DecisionId, @FactorObservationId, @Role)", new { decisionId, factorObservationId, role }); } public async Task AppendOutcomeAsync(OutcomeEvaluationRecord record) { using var conn = _connectionFactory.CreateConnection(); await conn.ExecuteAsync(@"INSERT INTO engine_history.outcome_evaluation (decision_id, horizon_days, evaluated_at, realized_return, benchmark_return, excess_return, outcome_class, evaluation_gate, provenance) VALUES (@DecisionId, @HorizonDays, @EvaluatedAt, @RealizedReturn, @BenchmarkReturn, @ExcessReturn, @OutcomeClass, @EvaluationGate, CAST(@ProvenanceJson AS jsonb)) ON CONFLICT (decision_id, horizon_days) DO UPDATE SET evaluated_at = EXCLUDED.evaluated_at, realized_return = EXCLUDED.realized_return, benchmark_return = EXCLUDED.benchmark_return, excess_return = EXCLUDED.excess_return, outcome_class = EXCLUDED.outcome_class, evaluation_gate = EXCLUDED.evaluation_gate, provenance = EXCLUDED.provenance", record); } }