consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,18 @@
using StellaOps.RiskEngine.Core.Contracts;
namespace StellaOps.RiskEngine.Core.Services;
/// <summary>
/// Persists risk score results for later retrieval/ledger projection.
/// Implementations must be deterministic and sideeffect free for identical inputs.
/// </summary>
public interface IRiskScoreResultStore
{
Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken);
/// <summary>
/// Attempts to read a previously persisted result by job identifier.
/// Implementations must be deterministic and side-effect free.
/// </summary>
bool TryGet(Guid jobId, out RiskScoreResult result);
}

View File

@@ -0,0 +1,64 @@
using StellaOps.RiskEngine.Core.Contracts;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Channels;
namespace StellaOps.RiskEngine.Core.Services;
/// <summary>
/// Deterministic FIFO queue for risk score jobs.
/// </summary>
public sealed class RiskScoreQueue
{
private readonly Channel<RiskScoreJob> channel;
public RiskScoreQueue(int? capacity = null)
{
if (capacity.HasValue)
{
var options = new BoundedChannelOptions(capacity.Value)
{
AllowSynchronousContinuations = false,
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.Wait
};
channel = Channel.CreateBounded<RiskScoreJob>(options);
}
else
{
var options = new UnboundedChannelOptions
{
AllowSynchronousContinuations = false,
SingleReader = true,
SingleWriter = false
};
channel = Channel.CreateUnbounded<RiskScoreJob>(options);
}
}
public ValueTask EnqueueAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var job = new RiskScoreJob(Guid.NewGuid(), request);
return channel.Writer.WriteAsync(job, cancellationToken);
}
/// <summary>
/// Enqueues a request and returns the assigned job id for later retrieval.
/// </summary>
public async ValueTask<Guid> EnqueueWithIdAsync(ScoreRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var job = new RiskScoreJob(Guid.NewGuid(), request);
await channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
return job.JobId;
}
public ValueTask<RiskScoreJob> DequeueAsync(CancellationToken cancellationToken) =>
channel.Reader.ReadAsync(cancellationToken);
public bool TryDequeue([NotNullWhen(true)] out RiskScoreJob? job) => channel.Reader.TryRead(out job);
}

View File

@@ -0,0 +1,86 @@
using StellaOps.RiskEngine.Core.Contracts;
using StellaOps.RiskEngine.Core.Providers;
namespace StellaOps.RiskEngine.Core.Services;
/// <summary>
/// Single-reader worker that pulls jobs from the queue and executes providers deterministically.
/// </summary>
public sealed class RiskScoreWorker
{
private readonly RiskScoreQueue queue;
private readonly IRiskScoreProviderRegistry registry;
private readonly IRiskScoreResultStore? resultStore;
private readonly TimeProvider timeProvider;
public RiskScoreWorker(
RiskScoreQueue queue,
IRiskScoreProviderRegistry registry,
IRiskScoreResultStore? resultStore = null,
TimeProvider? timeProvider = null)
{
this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
this.resultStore = resultStore;
this.timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<RiskScoreResult> ProcessNextAsync(CancellationToken cancellationToken)
{
var job = await queue.DequeueAsync(cancellationToken).ConfigureAwait(false);
var request = job.Request;
RiskScoreResult Build(double score, bool success, string? error) =>
new(
job.JobId,
request.Provider,
request.Subject,
Score: score,
Success: success,
Error: error,
request.Signals,
CompletedAtUtc: timeProvider.GetUtcNow());
if (!registry.TryGet(request.Provider, out var provider))
{
var missing = Build(0d, false, "Provider not registered");
await PersistAsync(missing, cancellationToken).ConfigureAwait(false);
return missing;
}
try
{
var score = await provider.ScoreAsync(request, cancellationToken).ConfigureAwait(false);
var success = Build(score, true, null);
await PersistAsync(success, cancellationToken).ConfigureAwait(false);
return success;
}
catch (Exception ex)
{
var failure = Build(0d, false, ex.Message);
await PersistAsync(failure, cancellationToken).ConfigureAwait(false);
return failure;
}
}
public async Task<IReadOnlyList<RiskScoreResult>> ProcessBatchAsync(int expectedCount, CancellationToken cancellationToken)
{
if (expectedCount < 0)
{
throw new ArgumentOutOfRangeException(nameof(expectedCount));
}
var results = new List<RiskScoreResult>(expectedCount);
for (var i = 0; i < expectedCount; i++)
{
results.Add(await ProcessNextAsync(cancellationToken).ConfigureAwait(false));
}
return results;
}
private Task PersistAsync(RiskScoreResult result, CancellationToken cancellationToken) =>
resultStore is null
? Task.CompletedTask
: resultStore.SaveAsync(result, cancellationToken);
}