using System.Diagnostics; using System.Text; using Npgsql; namespace StellaOps.Cli.Plugins.Aoc; public interface IAocVerificationService { Task VerifyAsync(AocVerifyOptions options, CancellationToken cancellationToken); } public interface IAocConnectionFactory { ValueTask OpenConnectionAsync(string connectionString, CancellationToken cancellationToken); } public sealed class NpgsqlConnectionFactory : IAocConnectionFactory { public async ValueTask 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 VerifyAsync( AocVerifyOptions options, CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); var violations = new List(); 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 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); } }); } }