save progress
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Plugins;
|
||||
|
||||
@@ -43,17 +45,20 @@ public sealed class AocCliCommandModule : ICliCommandModule
|
||||
{
|
||||
var aoc = new Command("aoc", "Append-Only Contract verification commands.");
|
||||
|
||||
var verify = BuildVerifyCommand(verboseOption, cancellationToken);
|
||||
var verify = BuildVerifyCommand(services, verboseOption, cancellationToken);
|
||||
aoc.Add(verify);
|
||||
|
||||
return aoc;
|
||||
}
|
||||
|
||||
private static Command BuildVerifyCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
private static Command BuildVerifyCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sinceOption = new Option<string>("--since", "-s")
|
||||
{
|
||||
Description = "Git commit SHA or ISO timestamp to verify from",
|
||||
Description = "ISO-8601 timestamp to verify from (UTC recommended)",
|
||||
Required = true
|
||||
};
|
||||
|
||||
@@ -96,94 +101,95 @@ public sealed class AocCliCommandModule : ICliCommandModule
|
||||
|
||||
verify.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var since = parseResult.GetValue(sinceOption)!;
|
||||
var postgres = parseResult.GetValue(postgresOption)!;
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var ndjson = parseResult.GetValue(ndjsonOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
var dryRun = parseResult.GetValue(dryRunOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
var options = new AocVerifyOptions
|
||||
var rawOptions = new AocVerifyRawOptions
|
||||
{
|
||||
Since = since,
|
||||
PostgresConnectionString = postgres,
|
||||
OutputPath = output,
|
||||
NdjsonPath = ndjson,
|
||||
Tenant = tenant,
|
||||
DryRun = dryRun,
|
||||
Verbose = verbose
|
||||
Since = parseResult.GetValue(sinceOption)!,
|
||||
PostgresConnectionString = parseResult.GetValue(postgresOption)!,
|
||||
OutputPath = parseResult.GetValue(outputOption),
|
||||
NdjsonPath = parseResult.GetValue(ndjsonOption),
|
||||
Tenant = parseResult.GetValue(tenantOption),
|
||||
DryRun = parseResult.GetValue(dryRunOption),
|
||||
Verbose = parseResult.GetValue(verboseOption)
|
||||
};
|
||||
|
||||
return await ExecuteVerifyAsync(options, ct);
|
||||
if (!AocVerifyOptionsParser.TryParse(rawOptions, out var options, out var errorMessage))
|
||||
{
|
||||
await Console.Error.WriteLineAsync(errorMessage);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var service = ResolveVerificationService(services);
|
||||
return await ExecuteVerifyAsync(options, service, Console.Out, Console.Error, ct);
|
||||
});
|
||||
|
||||
return verify;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteVerifyAsync(AocVerifyOptions options, CancellationToken cancellationToken)
|
||||
private static async Task<int> ExecuteVerifyAsync(
|
||||
AocVerifyOptions options,
|
||||
IAocVerificationService verificationService,
|
||||
TextWriter output,
|
||||
TextWriter error,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.WriteLine("AOC Verify starting...");
|
||||
Console.WriteLine($" Since: {options.Since}");
|
||||
Console.WriteLine($" Tenant: {options.Tenant ?? "(all)"}");
|
||||
Console.WriteLine($" Dry run: {options.DryRun}");
|
||||
await output.WriteLineAsync("AOC Verify starting...");
|
||||
await output.WriteLineAsync($" Since: {options.Since:O}");
|
||||
await output.WriteLineAsync($" Tenant: {options.Tenant ?? "(all)"}");
|
||||
await output.WriteLineAsync($" Dry run: {options.DryRun}");
|
||||
}
|
||||
|
||||
if (options.DryRun)
|
||||
{
|
||||
Console.WriteLine("Dry run mode - configuration validated successfully");
|
||||
await output.WriteLineAsync("Dry run mode - configuration validated successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var service = new AocVerificationService();
|
||||
var result = await service.VerifyAsync(options, cancellationToken);
|
||||
var result = await verificationService.VerifyAsync(options, cancellationToken);
|
||||
|
||||
// Write JSON output if requested
|
||||
if (!string.IsNullOrEmpty(options.OutputPath))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
EnsureOutputDirectory(options.OutputPath);
|
||||
var json = JsonSerializer.Serialize(result, JsonIndentedOptions);
|
||||
await File.WriteAllTextAsync(options.OutputPath, json, cancellationToken);
|
||||
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.WriteLine($"JSON report written to: {options.OutputPath}");
|
||||
await output.WriteLineAsync($"JSON report written to: {options.OutputPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Write NDJSON output if requested
|
||||
if (!string.IsNullOrEmpty(options.NdjsonPath))
|
||||
{
|
||||
var ndjsonLines = result.Violations.Select(v =>
|
||||
JsonSerializer.Serialize(v, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
|
||||
await File.WriteAllLinesAsync(options.NdjsonPath, ndjsonLines, cancellationToken);
|
||||
EnsureOutputDirectory(options.NdjsonPath);
|
||||
await WriteNdjsonAsync(options.NdjsonPath, result.Violations, cancellationToken);
|
||||
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.WriteLine($"NDJSON report written to: {options.NdjsonPath}");
|
||||
await output.WriteLineAsync($"NDJSON report written to: {options.NdjsonPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Output summary
|
||||
Console.WriteLine("AOC Verification Complete");
|
||||
Console.WriteLine($" Documents scanned: {result.DocumentsScanned}");
|
||||
Console.WriteLine($" Violations found: {result.ViolationCount}");
|
||||
Console.WriteLine($" Duration: {result.DurationMs}ms");
|
||||
await output.WriteLineAsync("AOC Verification Complete");
|
||||
await output.WriteLineAsync($" Documents scanned: {result.DocumentsScanned}");
|
||||
await output.WriteLineAsync($" Violations found: {result.ViolationCount}");
|
||||
await output.WriteLineAsync($" Duration: {result.DurationMs}ms");
|
||||
|
||||
if (result.ViolationCount > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Violations by type:");
|
||||
foreach (var group in result.Violations.GroupBy(v => v.Code))
|
||||
await output.WriteLineAsync();
|
||||
await output.WriteLineAsync("Violations by type:");
|
||||
foreach (var group in result.Violations
|
||||
.GroupBy(v => v.Code)
|
||||
.OrderBy(g => g.Key, StringComparer.Ordinal))
|
||||
{
|
||||
Console.WriteLine($" {group.Key}: {group.Count()}");
|
||||
await output.WriteLineAsync($" {group.Key}: {group.Count()}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,139 +197,58 @@ public sealed class AocCliCommandModule : ICliCommandModule
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error during verification: {ex.Message}");
|
||||
await error.WriteLineAsync($"Error during verification: {ex.Message}");
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.Error.WriteLine(ex.StackTrace);
|
||||
await error.WriteLineAsync(ex.ToString());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static IAocVerificationService ResolveVerificationService(IServiceProvider services)
|
||||
{
|
||||
var resolvedService = services.GetService<IAocVerificationService>();
|
||||
if (resolvedService is not null)
|
||||
{
|
||||
return resolvedService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for AOC verify command.
|
||||
/// </summary>
|
||||
public sealed class AocVerifyOptions
|
||||
{
|
||||
public required string Since { get; init; }
|
||||
public required string PostgresConnectionString { get; init; }
|
||||
public string? OutputPath { get; init; }
|
||||
public string? NdjsonPath { get; init; }
|
||||
public string? Tenant { get; init; }
|
||||
public bool DryRun { get; init; }
|
||||
public bool Verbose { get; init; }
|
||||
}
|
||||
var connectionFactory = services.GetService<IAocConnectionFactory>() ?? new NpgsqlConnectionFactory();
|
||||
var timeProvider = services.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
return new AocVerificationService(connectionFactory, timeProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for AOC verification operations.
|
||||
/// </summary>
|
||||
public sealed class AocVerificationService
|
||||
{
|
||||
public async Task<AocVerificationResult> VerifyAsync(
|
||||
AocVerifyOptions options,
|
||||
private static void EnsureOutputDirectory(string path)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteNdjsonAsync(
|
||||
string path,
|
||||
IReadOnlyList<AocViolation> violations,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var violations = new List<AocViolation>();
|
||||
var documentsScanned = 0;
|
||||
await using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
try
|
||||
foreach (var violation in violations)
|
||||
{
|
||||
await using var connection = new Npgsql.NpgsqlConnection(options.PostgresConnectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// Query for documents to verify
|
||||
var query = BuildVerificationQuery(options);
|
||||
await using var cmd = new Npgsql.NpgsqlCommand(query, connection);
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Tenant))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("tenant", options.Tenant);
|
||||
}
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
documentsScanned++;
|
||||
|
||||
// Check for AOC violations
|
||||
var documentId = reader.GetString(0);
|
||||
var hash = reader.IsDBNull(1) ? null : reader.GetString(1);
|
||||
var previousHash = reader.IsDBNull(2) ? null : reader.GetString(2);
|
||||
var createdAt = reader.GetDateTime(3);
|
||||
|
||||
// Verify hash chain integrity
|
||||
if (hash != null && previousHash != null)
|
||||
{
|
||||
// Placeholder: actual verification logic would check hash chain
|
||||
// For now, just record that we verified
|
||||
}
|
||||
}
|
||||
var line = JsonSerializer.Serialize(violation, JsonOptions);
|
||||
await writer.WriteLineAsync(line.AsMemory(), cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
violations.Add(new AocViolation
|
||||
{
|
||||
Code = "AOC-001",
|
||||
Message = $"Database verification failed: {ex.Message}",
|
||||
DocumentId = null,
|
||||
Severity = "error"
|
||||
});
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
return new AocVerificationResult
|
||||
{
|
||||
DocumentsScanned = documentsScanned,
|
||||
ViolationCount = violations.Count,
|
||||
Violations = violations,
|
||||
DurationMs = stopwatch.ElapsedMilliseconds,
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildVerificationQuery(AocVerifyOptions options)
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
// Placeholder query - actual implementation would query AOC tables
|
||||
var baseQuery = """
|
||||
SELECT id, hash, previous_hash, created_at
|
||||
FROM aoc_documents
|
||||
WHERE created_at >= @since
|
||||
""";
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Tenant))
|
||||
{
|
||||
baseQuery += " AND tenant_id = @tenant";
|
||||
}
|
||||
|
||||
baseQuery += " ORDER BY created_at ASC";
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of AOC verification.
|
||||
/// </summary>
|
||||
public sealed class AocVerificationResult
|
||||
{
|
||||
public int DocumentsScanned { get; init; }
|
||||
public int ViolationCount { get; init; }
|
||||
public IReadOnlyList<AocViolation> Violations { get; init; } = [];
|
||||
public long DurationMs { get; init; }
|
||||
public DateTimeOffset VerifiedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An AOC violation record.
|
||||
/// </summary>
|
||||
public sealed class AocViolation
|
||||
{
|
||||
public required string Code { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public string? DocumentId { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
private static readonly JsonSerializerOptions JsonIndentedOptions = new(JsonOptions)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace StellaOps.Cli.Plugins.Aoc;
|
||||
|
||||
/// <summary>
|
||||
/// Result of AOC verification.
|
||||
/// </summary>
|
||||
public sealed class AocVerificationResult
|
||||
{
|
||||
public int DocumentsScanned { get; init; }
|
||||
public int ViolationCount { get; init; }
|
||||
public IReadOnlyList<AocViolation> Violations { get; init; } = [];
|
||||
public long DurationMs { get; init; }
|
||||
public DateTimeOffset VerifiedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An AOC violation record.
|
||||
/// </summary>
|
||||
public sealed class AocViolation
|
||||
{
|
||||
public required string Code { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public string? DocumentId { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Cli.Plugins.Aoc;
|
||||
|
||||
public sealed class AocVerifyRawOptions
|
||||
{
|
||||
public required string Since { get; init; }
|
||||
public required string PostgresConnectionString { get; init; }
|
||||
public string? OutputPath { get; init; }
|
||||
public string? NdjsonPath { get; init; }
|
||||
public string? Tenant { get; init; }
|
||||
public bool DryRun { get; init; }
|
||||
public bool Verbose { get; init; }
|
||||
}
|
||||
|
||||
public sealed class AocVerifyOptions
|
||||
{
|
||||
public required DateTimeOffset Since { get; init; }
|
||||
public required string PostgresConnectionString { get; init; }
|
||||
public string? OutputPath { get; init; }
|
||||
public string? NdjsonPath { get; init; }
|
||||
public string? Tenant { get; init; }
|
||||
public bool DryRun { get; init; }
|
||||
public bool Verbose { get; init; }
|
||||
}
|
||||
|
||||
public static class AocVerifyOptionsParser
|
||||
{
|
||||
private static readonly Regex CommitShaRegex = new("^[a-fA-F0-9]{7,40}$", RegexOptions.Compiled);
|
||||
|
||||
public static bool TryParse(
|
||||
AocVerifyRawOptions raw,
|
||||
out AocVerifyOptions options,
|
||||
out string errorMessage)
|
||||
{
|
||||
options = default!;
|
||||
errorMessage = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(raw.PostgresConnectionString))
|
||||
{
|
||||
errorMessage = "PostgreSQL connection string is required.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseSince(raw.Since, out var since, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateOutputPaths(raw.OutputPath, raw.NdjsonPath, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
options = new AocVerifyOptions
|
||||
{
|
||||
Since = since,
|
||||
PostgresConnectionString = raw.PostgresConnectionString,
|
||||
OutputPath = raw.OutputPath,
|
||||
NdjsonPath = raw.NdjsonPath,
|
||||
Tenant = raw.Tenant,
|
||||
DryRun = raw.DryRun,
|
||||
Verbose = raw.Verbose
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseSince(
|
||||
string value,
|
||||
out DateTimeOffset since,
|
||||
out string errorMessage)
|
||||
{
|
||||
errorMessage = string.Empty;
|
||||
since = default;
|
||||
|
||||
if (DateTimeOffset.TryParse(
|
||||
value,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces,
|
||||
out var parsed))
|
||||
{
|
||||
since = parsed.ToUniversalTime();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CommitShaRegex.IsMatch(value))
|
||||
{
|
||||
errorMessage = "Commit-based --since values are not supported yet; provide an ISO-8601 timestamp.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = "Invalid --since value; expected an ISO-8601 timestamp.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ValidateOutputPaths(
|
||||
string? outputPath,
|
||||
string? ndjsonPath,
|
||||
out string errorMessage)
|
||||
{
|
||||
errorMessage = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputPath) && string.IsNullOrWhiteSpace(ndjsonPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath) && Path.EndsInDirectorySeparator(outputPath))
|
||||
{
|
||||
errorMessage = "--output must be a file path, not a directory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ndjsonPath) && Path.EndsInDirectorySeparator(ndjsonPath))
|
||||
{
|
||||
errorMessage = "--ndjson must be a file path, not a directory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath) && !string.IsNullOrWhiteSpace(ndjsonPath))
|
||||
{
|
||||
var outputFullPath = Path.GetFullPath(outputPath);
|
||||
var ndjsonFullPath = Path.GetFullPath(ndjsonPath);
|
||||
if (string.Equals(outputFullPath, ndjsonFullPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errorMessage = "--output and --ndjson must point to different files.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<PluginOutputDirectory>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\plugins\cli\StellaOps.Cli.Plugins.Aoc\'))</PluginOutputDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -25,9 +25,13 @@
|
||||
|
||||
<Target Name="CopyPluginBinaries" AfterTargets="Build">
|
||||
<MakeDir Directories="$(PluginOutputDirectory)" />
|
||||
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(PluginOutputDirectory)" />
|
||||
<Copy SourceFiles="$(TargetDir)$(TargetName).pdb"
|
||||
DestinationFolder="$(PluginOutputDirectory)"
|
||||
Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
|
||||
<ItemGroup>
|
||||
<PluginArtifacts Include="$(TargetDir)$(TargetFileName)" />
|
||||
<PluginArtifacts Include="$(TargetDir)$(TargetName).pdb" Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
|
||||
<PluginArtifacts Include="$(TargetDir)$(TargetName).deps.json" Condition="Exists('$(TargetDir)$(TargetName).deps.json')" />
|
||||
<PluginArtifacts Include="$(TargetDir)$(TargetName).runtimeconfig.json" Condition="Exists('$(TargetDir)$(TargetName).runtimeconfig.json')" />
|
||||
<PluginArtifacts Include="@(ReferenceCopyLocalPaths)" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(PluginArtifacts)" DestinationFolder="$(PluginOutputDirectory)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0138-M | DONE | Maintainability audit for StellaOps.Cli.Plugins.Aoc. |
|
||||
| AUDIT-0138-T | DONE | Test coverage audit for StellaOps.Cli.Plugins.Aoc. |
|
||||
| AUDIT-0138-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0138-A | DONE | Applied option validation, query binding, deterministic output, and tests. |
|
||||
|
||||
Reference in New Issue
Block a user