consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.RiskEngine.Infrastructure;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
|
||||
<ProjectReference Include="..\..\StellaOps.RiskEngine.Core\StellaOps.RiskEngine.Core.csproj"/>
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
using StellaOps.RiskEngine.Core.Contracts;
|
||||
using StellaOps.RiskEngine.Core.Services;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.RiskEngine.Infrastructure.Stores;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic in-memory store for risk score results.
|
||||
/// Used for offline/ephemeral runs and testing until ledger integration lands.
|
||||
/// </summary>
|
||||
public sealed class InMemoryRiskScoreResultStore : IRiskScoreResultStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, RiskScoreResult> results = new();
|
||||
private readonly ConcurrentQueue<Guid> order = new();
|
||||
|
||||
public Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (results.TryAdd(result.JobId, result))
|
||||
{
|
||||
order.Enqueue(result.JobId);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IReadOnlyList<RiskScoreResult> Snapshot() =>
|
||||
order.Select(id => results[id]).ToArray();
|
||||
|
||||
public bool TryGet(Guid jobId, out RiskScoreResult result) =>
|
||||
results.TryGetValue(jobId, out result!);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using StellaOps.RiskEngine.Core.Contracts;
|
||||
using StellaOps.RiskEngine.Core.Services;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.RiskEngine.Infrastructure.Stores;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL-backed risk score result store for durable production retrieval.
|
||||
/// </summary>
|
||||
public sealed class PostgresRiskScoreResultStore : IRiskScoreResultStore, IAsyncDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly object _initGate = new();
|
||||
private bool _tableInitialized;
|
||||
|
||||
public PostgresRiskScoreResultStore(string connectionString)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
|
||||
_dataSource = NpgsqlDataSource.Create(connectionString);
|
||||
}
|
||||
|
||||
public async Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO riskengine.risk_score_results (
|
||||
job_id,
|
||||
provider,
|
||||
subject,
|
||||
score,
|
||||
success,
|
||||
error,
|
||||
signals,
|
||||
completed_at
|
||||
) VALUES (
|
||||
@job_id,
|
||||
@provider,
|
||||
@subject,
|
||||
@score,
|
||||
@success,
|
||||
@error,
|
||||
@signals,
|
||||
@completed_at
|
||||
)
|
||||
ON CONFLICT (job_id) DO UPDATE SET
|
||||
provider = EXCLUDED.provider,
|
||||
subject = EXCLUDED.subject,
|
||||
score = EXCLUDED.score,
|
||||
success = EXCLUDED.success,
|
||||
error = EXCLUDED.error,
|
||||
signals = EXCLUDED.signals,
|
||||
completed_at = EXCLUDED.completed_at;
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("job_id", result.JobId);
|
||||
command.Parameters.AddWithValue("provider", result.Provider);
|
||||
command.Parameters.AddWithValue("subject", result.Subject);
|
||||
command.Parameters.AddWithValue("score", result.Score);
|
||||
command.Parameters.AddWithValue("success", result.Success);
|
||||
command.Parameters.AddWithValue("error", (object?)result.Error ?? DBNull.Value);
|
||||
command.Parameters.Add("signals", NpgsqlDbType.Jsonb).Value = JsonSerializer.Serialize(result.Signals, JsonOptions);
|
||||
command.Parameters.AddWithValue("completed_at", result.CompletedAtUtc);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public bool TryGet(Guid jobId, out RiskScoreResult result)
|
||||
{
|
||||
EnsureTable();
|
||||
|
||||
const string sql = """
|
||||
SELECT provider, subject, score, success, error, signals, completed_at
|
||||
FROM riskengine.risk_score_results
|
||||
WHERE job_id = @job_id;
|
||||
""";
|
||||
|
||||
using var connection = _dataSource.OpenConnection();
|
||||
using var command = new NpgsqlCommand(sql, connection);
|
||||
command.Parameters.AddWithValue("job_id", jobId);
|
||||
|
||||
using var reader = command.ExecuteReader();
|
||||
if (!reader.Read())
|
||||
{
|
||||
result = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = reader.GetString(0);
|
||||
var subject = reader.GetString(1);
|
||||
var score = reader.GetDouble(2);
|
||||
var success = reader.GetBoolean(3);
|
||||
var error = reader.IsDBNull(4) ? null : reader.GetString(4);
|
||||
var signalsJson = reader.GetString(5);
|
||||
var completedAt = reader.GetFieldValue<DateTimeOffset>(6);
|
||||
|
||||
var signals = JsonSerializer.Deserialize<Dictionary<string, double>>(signalsJson, JsonOptions)
|
||||
?? new Dictionary<string, double>(StringComparer.Ordinal);
|
||||
|
||||
result = new RiskScoreResult(
|
||||
jobId,
|
||||
provider,
|
||||
subject,
|
||||
score,
|
||||
success,
|
||||
error,
|
||||
signals,
|
||||
completedAt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task EnsureTableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_initGate)
|
||||
{
|
||||
if (_tableInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const string ddl = """
|
||||
CREATE SCHEMA IF NOT EXISTS riskengine;
|
||||
CREATE TABLE IF NOT EXISTS riskengine.risk_score_results (
|
||||
job_id UUID PRIMARY KEY,
|
||||
provider TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
score DOUBLE PRECISION NOT NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
error TEXT NULL,
|
||||
signals JSONB NOT NULL,
|
||||
completed_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_risk_score_results_completed_at
|
||||
ON riskengine.risk_score_results (completed_at DESC);
|
||||
""";
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(ddl, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
lock (_initGate)
|
||||
{
|
||||
_tableInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureTable()
|
||||
{
|
||||
lock (_initGate)
|
||||
{
|
||||
if (_tableInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const string ddl = """
|
||||
CREATE SCHEMA IF NOT EXISTS riskengine;
|
||||
CREATE TABLE IF NOT EXISTS riskengine.risk_score_results (
|
||||
job_id UUID PRIMARY KEY,
|
||||
provider TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
score DOUBLE PRECISION NOT NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
error TEXT NULL,
|
||||
signals JSONB NOT NULL,
|
||||
completed_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_risk_score_results_completed_at
|
||||
ON riskengine.risk_score_results (completed_at DESC);
|
||||
""";
|
||||
|
||||
using var connection = _dataSource.OpenConnection();
|
||||
using var command = new NpgsqlCommand(ddl, connection);
|
||||
command.ExecuteNonQuery();
|
||||
|
||||
lock (_initGate)
|
||||
{
|
||||
_tableInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# StellaOps.RiskEngine.Infrastructure Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Infrastructure/StellaOps.RiskEngine.Infrastructure.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| SPRINT-312-005 | DONE | Added `PostgresRiskScoreResultStore` with schema/bootstrap and deterministic upsert/read behavior. |
|
||||
Reference in New Issue
Block a user