Files
git.stella-ops.org/src/Cli/__Libraries/StellaOps.Cli.Plugins.Aoc/AocVerificationService.cs
StellaOps Bot 83c37243e0 save progress
2026-01-03 11:02:24 +02:00

115 lines
3.8 KiB
C#

using System.Diagnostics;
using System.Text;
using Npgsql;
namespace StellaOps.Cli.Plugins.Aoc;
public interface IAocVerificationService
{
Task<AocVerificationResult> VerifyAsync(AocVerifyOptions options, CancellationToken cancellationToken);
}
public interface IAocConnectionFactory
{
ValueTask<NpgsqlConnection> OpenConnectionAsync(string connectionString, CancellationToken cancellationToken);
}
public sealed class NpgsqlConnectionFactory : IAocConnectionFactory
{
public async ValueTask<NpgsqlConnection> OpenConnectionAsync(string connectionString, CancellationToken cancellationToken)
{
var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
return connection;
}
}
public sealed class AocVerificationService : IAocVerificationService
{
private readonly IAocConnectionFactory _connectionFactory;
private readonly TimeProvider _timeProvider;
public AocVerificationService(IAocConnectionFactory connectionFactory, TimeProvider timeProvider)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
public async Task<AocVerificationResult> VerifyAsync(
AocVerifyOptions options,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var violations = new List<AocViolation>();
var documentsScanned = 0;
var query = AocVerificationQueryBuilder.Build(options);
await using var connection = await _connectionFactory.OpenConnectionAsync(
options.PostgresConnectionString,
cancellationToken).ConfigureAwait(false);
await using var cmd = new NpgsqlCommand(query.Sql, connection);
query.BindParameters(cmd);
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
documentsScanned++;
_ = reader.GetString(0);
var hash = reader.IsDBNull(1) ? null : reader.GetString(1);
var previousHash = reader.IsDBNull(2) ? null : reader.GetString(2);
_ = reader.GetDateTime(3);
if (hash is null || previousHash is null)
{
continue;
}
// TODO: implement hash chain verification and emit violations.
}
stopwatch.Stop();
return new AocVerificationResult
{
DocumentsScanned = documentsScanned,
ViolationCount = violations.Count,
Violations = violations,
DurationMs = stopwatch.ElapsedMilliseconds,
VerifiedAt = _timeProvider.GetUtcNow()
};
}
}
public readonly record struct AocVerificationQuery(string Sql, Action<NpgsqlCommand> BindParameters);
public static class AocVerificationQueryBuilder
{
public static AocVerificationQuery Build(AocVerifyOptions options)
{
var builder = new StringBuilder();
builder.AppendLine("SELECT id, hash, previous_hash, created_at");
builder.AppendLine("FROM aoc_documents");
builder.AppendLine("WHERE created_at >= @since");
if (!string.IsNullOrWhiteSpace(options.Tenant))
{
builder.AppendLine("AND tenant_id = @tenant");
}
builder.AppendLine("ORDER BY created_at ASC");
return new AocVerificationQuery(builder.ToString(), command =>
{
command.Parameters.AddWithValue("since", options.Since.UtcDateTime);
if (!string.IsNullOrWhiteSpace(options.Tenant))
{
command.Parameters.AddWithValue("tenant", options.Tenant);
}
});
}
}